openxiangda 1.0.176 → 1.0.177
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 +4 -2
- package/lib/cli.js +466 -57
- package/lib/publish-lease-heartbeat.js +1 -1
- package/lib/publish-lease.js +2 -2
- package/lib/task-status.js +81 -18
- package/openxiangda-skills/SKILL.md +8 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +3 -1
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +3 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
package/README.md
CHANGED
|
@@ -123,13 +123,15 @@ Domestic npm mirrors may lag and return an older OpenXiangda version. `openxiang
|
|
|
123
123
|
|
|
124
124
|
## AI design gate and resource CLI
|
|
125
125
|
|
|
126
|
-
Architecture-class requests
|
|
126
|
+
Architecture-class requests run the relevant design gate first. AI agents pause only for unresolved business, security, or data choices that would change the implementation. When the user already supplied concrete requirements and acceptance criteria, agents record the structured SDD scope and implement without a long design essay or duplicate confirmation.
|
|
127
127
|
|
|
128
128
|
Risk is tiered. Read-only/docs/tests are L0. Narrow reversible fixes are L1 and use `sdd quick`; L2/L3 retain explicit approval and exact structured coverage. SDD is structured-first by default: `change.json`, `coverage.json`, and `release.json` are authoritative, while proposal/design/tasks/evidence/spec prose is generated only by `openxiangda sdd render <change>` or `documentationMode: 'full'`. Workspaces that intentionally require prose completion may set `strictDocumentation: true`. Actual argv, exact scope, mainline identity, lease, CAS, destructive operations, Secrets, and Root App atomic activation remain hard gates.
|
|
129
129
|
|
|
130
130
|
L1 quick records are generated in a compact form and should not be expanded into design essays. Mainline bundles may keep `<profile>` in reviewed command templates; `release publish --profile <name>` binds the real profile to actual argv without rewriting tracked SDD. React SPA page codes remain logical coverage and activate through one Runtime child. `release app-head` and `runtime releases` return compact summaries by default; add `--full` for the complete manifest.
|
|
131
131
|
|
|
132
|
-
Every concurrent task uses its own Git worktree/branch and development change. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
|
|
132
|
+
Every concurrent task uses its own Git worktree/branch and development change. The canonical main checkout is reserved for integration and release; tasks never stash/restore each other's files to make it publishable. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
|
|
133
|
+
|
|
134
|
+
Agent investigation is bounded to one complete CodeGraph survey plus at most one focused follow-up, and ordinary work loads one domain skill. Unchanged lease/build waits use `task status --watch` and produce updates only on material transitions.
|
|
133
135
|
|
|
134
136
|
Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
|
|
135
137
|
|
package/lib/cli.js
CHANGED
|
@@ -170,13 +170,32 @@ let readOnlyHttpGuardDepth = 0;
|
|
|
170
170
|
let readOnlyHttpGuardLabel = null;
|
|
171
171
|
let activePublishRequestContext = null;
|
|
172
172
|
let activePublishLeaseHeartbeat = null;
|
|
173
|
+
let activeCommandScopedPublishLease = null;
|
|
173
174
|
const genericGitBasePlanCache = new Map();
|
|
174
175
|
|
|
175
176
|
async function main(argv) {
|
|
177
|
+
let commandError = null;
|
|
176
178
|
try {
|
|
177
179
|
return await mainImpl(argv);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
commandError = error;
|
|
182
|
+
throw error;
|
|
178
183
|
} finally {
|
|
179
184
|
await stopActivePublishLeaseHeartbeat();
|
|
185
|
+
try {
|
|
186
|
+
await finalizeCommandScopedPublishLease({
|
|
187
|
+
succeeded: commandError === null,
|
|
188
|
+
});
|
|
189
|
+
} catch (cleanupError) {
|
|
190
|
+
if (!commandError) throw cleanupError;
|
|
191
|
+
commandError.publishLeaseCleanupError = {
|
|
192
|
+
code: cleanupError?.code || 'PUBLISH_LEASE_CLEANUP_FAILED',
|
|
193
|
+
message: maskText(cleanupError?.message || String(cleanupError)),
|
|
194
|
+
};
|
|
195
|
+
warn(
|
|
196
|
+
`发布命令失败后的租约清理也未完成:${commandError.publishLeaseCleanupError.message}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
180
199
|
activePublishRequestContext = null;
|
|
181
200
|
}
|
|
182
201
|
}
|
|
@@ -1005,6 +1024,10 @@ function releaseExecutionFile(changeId) {
|
|
|
1005
1024
|
return path.join(releaseExecutionDir(changeId), 'execution.json');
|
|
1006
1025
|
}
|
|
1007
1026
|
|
|
1027
|
+
function directPublishReceiptFile(changeId) {
|
|
1028
|
+
return path.join(releaseExecutionDir(changeId), 'direct-publish.json');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1008
1031
|
function writePrivateJsonAtomic(file, value) {
|
|
1009
1032
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1010
1033
|
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -1026,6 +1049,39 @@ function readReleaseExecution(changeId) {
|
|
|
1026
1049
|
}
|
|
1027
1050
|
}
|
|
1028
1051
|
|
|
1052
|
+
function readDirectPublishReceipt(changeId) {
|
|
1053
|
+
const file = directPublishReceiptFile(changeId);
|
|
1054
|
+
if (!fs.existsSync(file)) return null;
|
|
1055
|
+
try {
|
|
1056
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1057
|
+
} catch (error) {
|
|
1058
|
+
fail(
|
|
1059
|
+
`DIRECT_PUBLISH_RECEIPT_INVALID: ${path.relative(process.cwd(), file)} 无法读取: ${error.message}`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function writeDirectPublishReceipt(changeId, input = {}) {
|
|
1065
|
+
if (!changeId) return null;
|
|
1066
|
+
const receipt = {
|
|
1067
|
+
schemaVersion: 'openxiangda_direct_publish_receipt_v1',
|
|
1068
|
+
changeId,
|
|
1069
|
+
status: 'completed',
|
|
1070
|
+
command: input.command || null,
|
|
1071
|
+
writeAttempted: Boolean(input.writeAttempted),
|
|
1072
|
+
selectors: unique(input.selectors || []).sort(),
|
|
1073
|
+
sourceRevision: input.sourceRevision || null,
|
|
1074
|
+
result: input.result || null,
|
|
1075
|
+
completedAt: new Date().toISOString(),
|
|
1076
|
+
};
|
|
1077
|
+
const file = directPublishReceiptFile(changeId);
|
|
1078
|
+
writePrivateJsonAtomic(file, receipt);
|
|
1079
|
+
return {
|
|
1080
|
+
file: path.relative(process.cwd(), file).replace(/\\/g, '/'),
|
|
1081
|
+
receipt,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1029
1085
|
function summarizeReleaseStepResult(result) {
|
|
1030
1086
|
const value =
|
|
1031
1087
|
result && typeof result === 'object' && !Array.isArray(result)
|
|
@@ -1181,6 +1237,8 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1181
1237
|
const deadline = startedAt + waitSeconds * 1000;
|
|
1182
1238
|
const clientSessionId = createPublishClientSessionId();
|
|
1183
1239
|
let attempts = 0;
|
|
1240
|
+
let lastNoticeFingerprint = '';
|
|
1241
|
+
let lastNoticeAt = 0;
|
|
1184
1242
|
while (true) {
|
|
1185
1243
|
const remote = await fetchPublishLeaseStatus(
|
|
1186
1244
|
config,
|
|
@@ -1191,10 +1249,11 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1191
1249
|
return { waitedMs: Date.now() - startedAt, attempts, remote };
|
|
1192
1250
|
}
|
|
1193
1251
|
const remoteHolder =
|
|
1194
|
-
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1195
|
-
remote?.holder?.changeId ||
|
|
1196
1252
|
remote?.changeId ||
|
|
1253
|
+
remote?.clientSessionId ||
|
|
1254
|
+
remote?.holder?.changeId ||
|
|
1197
1255
|
remote?.holder?.clientSessionId ||
|
|
1256
|
+
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1198
1257
|
'another release';
|
|
1199
1258
|
if (Date.now() >= deadline) {
|
|
1200
1259
|
fail(
|
|
@@ -1210,9 +1269,20 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1210
1269
|
30000,
|
|
1211
1270
|
Math.max(1000, deadline - Date.now())
|
|
1212
1271
|
);
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1272
|
+
const noticeFingerprint = [
|
|
1273
|
+
remoteHolder,
|
|
1274
|
+
remote?.leaseId || '',
|
|
1275
|
+
].join(':');
|
|
1276
|
+
if (
|
|
1277
|
+
noticeFingerprint !== lastNoticeFingerprint ||
|
|
1278
|
+
Date.now() - lastNoticeAt >= 120_000
|
|
1279
|
+
) {
|
|
1280
|
+
warn(
|
|
1281
|
+
`发布队列等待中: app=${target.appType} holder=${remoteHolder} retry=${Math.ceil(delayMs / 1000)}s${remote?.expiresAt ? ` expiresAt=${remote.expiresAt}` : ''}`
|
|
1282
|
+
);
|
|
1283
|
+
lastNoticeFingerprint = noticeFingerprint;
|
|
1284
|
+
lastNoticeAt = Date.now();
|
|
1285
|
+
}
|
|
1216
1286
|
await sleep(delayMs);
|
|
1217
1287
|
}
|
|
1218
1288
|
}
|
|
@@ -1443,6 +1513,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1443
1513
|
execution: null,
|
|
1444
1514
|
};
|
|
1445
1515
|
}
|
|
1516
|
+
const plannedSourceRevision = prepareReleaseSourceRevision({
|
|
1517
|
+
cwd: process.cwd(),
|
|
1518
|
+
});
|
|
1446
1519
|
const existing = readReleaseExecution(changeId);
|
|
1447
1520
|
if (
|
|
1448
1521
|
existing &&
|
|
@@ -1462,6 +1535,11 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1462
1535
|
'RELEASE_WRITE_REVIEW_REQUIRED: 上次写请求结果不确定;请先只读核对 staged release,确认可重试后追加 --resume-after-review'
|
|
1463
1536
|
);
|
|
1464
1537
|
}
|
|
1538
|
+
if (existing?.releaseSourceRevision) {
|
|
1539
|
+
assertReleaseSourceRevisionStable(existing.releaseSourceRevision, {
|
|
1540
|
+
cwd: process.cwd(),
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1465
1543
|
const now = new Date().toISOString();
|
|
1466
1544
|
const execution = existing || {
|
|
1467
1545
|
schemaVersion: 'openxiangda_release_execution_v1',
|
|
@@ -1469,6 +1547,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1469
1547
|
profile: target.profileName,
|
|
1470
1548
|
changeId,
|
|
1471
1549
|
planHash,
|
|
1550
|
+
releaseSourceRevision: plannedSourceRevision,
|
|
1472
1551
|
status: 'planned',
|
|
1473
1552
|
createdAt: now,
|
|
1474
1553
|
updatedAt: now,
|
|
@@ -1493,6 +1572,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1493
1572
|
},
|
|
1494
1573
|
],
|
|
1495
1574
|
};
|
|
1575
|
+
if (!execution.releaseSourceRevision) {
|
|
1576
|
+
execution.releaseSourceRevision = plannedSourceRevision;
|
|
1577
|
+
}
|
|
1496
1578
|
if (execution.status === 'completed') {
|
|
1497
1579
|
return {
|
|
1498
1580
|
completed: true,
|
|
@@ -1662,7 +1744,7 @@ async function release(args) {
|
|
|
1662
1744
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
1663
1745
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
1664
1746
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
1665
|
-
' - 默认 TTL 为
|
|
1747
|
+
' - 默认 TTL 为 120 秒;活动发布每 30 秒心跳并在到期前续租,客户端退出后孤儿租约最多阻塞约 2 分钟。',
|
|
1666
1748
|
' - begin 仅接受 clean 且与权威远端默认 main/master 完全一致的 HEAD;feature branch 先合并并 push,再由主分支一次发布。',
|
|
1667
1749
|
' - 因发布源已在远端主线,成功激活后 integration-status 会立即通过,end 不再等待补合并。',
|
|
1668
1750
|
' - integration-status --check 可作为 CI 门禁;尚未回合时返回非零。',
|
|
@@ -2080,7 +2162,14 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2080
2162
|
process.cwd(),
|
|
2081
2163
|
changeId
|
|
2082
2164
|
);
|
|
2083
|
-
const
|
|
2165
|
+
const execution = readReleaseExecution(changeId);
|
|
2166
|
+
const directPublish = readDirectPublishReceipt(changeId);
|
|
2167
|
+
const sourceRevision =
|
|
2168
|
+
execution?.releaseSourceRevision ||
|
|
2169
|
+
execution?.releaseContext?.releaseSourceRevision ||
|
|
2170
|
+
directPublish?.sourceRevision ||
|
|
2171
|
+
null;
|
|
2172
|
+
let integration = taskResult
|
|
2084
2173
|
? inspectIntegrationBundleCommits(
|
|
2085
2174
|
{
|
|
2086
2175
|
requiredCommits: [
|
|
@@ -2095,13 +2184,26 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2095
2184
|
{ cwd: process.cwd() }
|
|
2096
2185
|
)
|
|
2097
2186
|
: null;
|
|
2098
|
-
|
|
2187
|
+
let integrationError = null;
|
|
2188
|
+
if (!integration && sourceRevision) {
|
|
2189
|
+
try {
|
|
2190
|
+
integration = inspectReleaseSourceIntegration(sourceRevision, {
|
|
2191
|
+
cwd: process.cwd(),
|
|
2192
|
+
});
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
integrationError = {
|
|
2195
|
+
code: error?.code || 'RELEASE_INTEGRATION_STATUS_UNAVAILABLE',
|
|
2196
|
+
message: maskText(error?.message || String(error)),
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2099
2200
|
const localLease = getStoredPublishLease(target);
|
|
2100
2201
|
const baseline = getStoredChangeBaseline(target, {
|
|
2101
2202
|
access: 'reconciliation-read',
|
|
2102
2203
|
});
|
|
2103
2204
|
const clientSessionId =
|
|
2104
2205
|
localLease?.clientSessionId ||
|
|
2206
|
+
execution?.releaseContext?.clientSessionId ||
|
|
2105
2207
|
baseline?.clientSessionId ||
|
|
2106
2208
|
createPublishClientSessionId();
|
|
2107
2209
|
let remoteLease = null;
|
|
@@ -2145,6 +2247,8 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2145
2247
|
taskResult,
|
|
2146
2248
|
integration,
|
|
2147
2249
|
execution,
|
|
2250
|
+
directPublish,
|
|
2251
|
+
sourceRevision,
|
|
2148
2252
|
remoteLease,
|
|
2149
2253
|
postCommit,
|
|
2150
2254
|
}),
|
|
@@ -2152,6 +2256,7 @@ async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
|
2152
2256
|
diagnostics: {
|
|
2153
2257
|
remoteLeaseError,
|
|
2154
2258
|
postCommitError,
|
|
2259
|
+
integrationError,
|
|
2155
2260
|
},
|
|
2156
2261
|
};
|
|
2157
2262
|
}
|
|
@@ -3918,6 +4023,7 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
3918
4023
|
const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
|
|
3919
4024
|
const changeId = readStringFlag(flags, 'change');
|
|
3920
4025
|
const storedLease = getStoredPublishLease(target);
|
|
4026
|
+
let acquiredByCommand = false;
|
|
3921
4027
|
assertOrClaimWorktreeOwner({
|
|
3922
4028
|
cwd: process.cwd(),
|
|
3923
4029
|
changeId: changeId || storedLease?.changeId || 'unscoped-publish',
|
|
@@ -3981,6 +4087,7 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
3981
4087
|
...flags,
|
|
3982
4088
|
'client-session-id': clientSessionId,
|
|
3983
4089
|
});
|
|
4090
|
+
acquiredByCommand = true;
|
|
3984
4091
|
} catch (error) {
|
|
3985
4092
|
clearRejectedPendingBaseline(target, baseline, error);
|
|
3986
4093
|
throw error;
|
|
@@ -4017,6 +4124,17 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
4017
4124
|
if (shouldRenewPublishLease(lease)) {
|
|
4018
4125
|
lease = await heartbeat.beforeWrite({ forceRenew: true });
|
|
4019
4126
|
}
|
|
4127
|
+
if (acquiredByCommand) {
|
|
4128
|
+
activeCommandScopedPublishLease = {
|
|
4129
|
+
config,
|
|
4130
|
+
target,
|
|
4131
|
+
leaseId: lease.leaseId,
|
|
4132
|
+
changeId: lease.changeId || changeId,
|
|
4133
|
+
writeAttempted: false,
|
|
4134
|
+
retain: false,
|
|
4135
|
+
retentionReason: null,
|
|
4136
|
+
};
|
|
4137
|
+
}
|
|
4020
4138
|
return lease;
|
|
4021
4139
|
}
|
|
4022
4140
|
|
|
@@ -4084,6 +4202,112 @@ async function stopActivePublishLeaseHeartbeat() {
|
|
|
4084
4202
|
if (active?.heartbeat) await active.heartbeat.stop();
|
|
4085
4203
|
}
|
|
4086
4204
|
|
|
4205
|
+
function markCommandScopedPublishWriteAttempted() {
|
|
4206
|
+
if (activeCommandScopedPublishLease) {
|
|
4207
|
+
activeCommandScopedPublishLease.writeAttempted = true;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
function retainCommandScopedPublishLease(reason = 'staged-release') {
|
|
4212
|
+
if (!activeCommandScopedPublishLease) return false;
|
|
4213
|
+
activeCommandScopedPublishLease.retain = true;
|
|
4214
|
+
activeCommandScopedPublishLease.retentionReason = String(reason || 'staged-release');
|
|
4215
|
+
return true;
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
async function finalizeCommandScopedPublishLease(options = {}) {
|
|
4219
|
+
const active = activeCommandScopedPublishLease;
|
|
4220
|
+
if (!active) return null;
|
|
4221
|
+
activeCommandScopedPublishLease = null;
|
|
4222
|
+
if (active.retain) {
|
|
4223
|
+
return {
|
|
4224
|
+
managed: true,
|
|
4225
|
+
released: false,
|
|
4226
|
+
retained: true,
|
|
4227
|
+
reason: active.retentionReason,
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
4230
|
+
if (options.succeeded === false && active.writeAttempted) {
|
|
4231
|
+
warn(
|
|
4232
|
+
`PUBLISH_WRITE_REVIEW_REQUIRED: change=${active.changeId || '-'} 的写请求结果未完整确认,租约已保留供只读核对和恢复`
|
|
4233
|
+
);
|
|
4234
|
+
return {
|
|
4235
|
+
managed: true,
|
|
4236
|
+
released: false,
|
|
4237
|
+
retained: true,
|
|
4238
|
+
reason: 'write-result-review',
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
await stopActivePublishLeaseHeartbeat();
|
|
4243
|
+
const stored = getStoredPublishLease(active.target);
|
|
4244
|
+
if (!stored || stored.leaseId !== active.leaseId) {
|
|
4245
|
+
return {
|
|
4246
|
+
managed: true,
|
|
4247
|
+
released: false,
|
|
4248
|
+
retained: false,
|
|
4249
|
+
reason: 'local-lease-already-cleared',
|
|
4250
|
+
};
|
|
4251
|
+
}
|
|
4252
|
+
const baseline = getStoredChangeBaseline(active.target, {
|
|
4253
|
+
access: 'reconciliation-read',
|
|
4254
|
+
});
|
|
4255
|
+
const integration = assertReleaseSourceIntegrated(
|
|
4256
|
+
baseline?.releaseSourceRevision,
|
|
4257
|
+
{ cwd: process.cwd() }
|
|
4258
|
+
);
|
|
4259
|
+
try {
|
|
4260
|
+
await requestWithAuth(
|
|
4261
|
+
active.config,
|
|
4262
|
+
active.target.profileName,
|
|
4263
|
+
publishLeaseApiPath(
|
|
4264
|
+
active.target,
|
|
4265
|
+
`/${encodeURIComponent(active.leaseId)}/release`
|
|
4266
|
+
),
|
|
4267
|
+
{
|
|
4268
|
+
method: 'POST',
|
|
4269
|
+
body: {
|
|
4270
|
+
completion:
|
|
4271
|
+
options.succeeded === false
|
|
4272
|
+
? 'prewrite-failed'
|
|
4273
|
+
: 'direct-publish-completed',
|
|
4274
|
+
sourceCommit: integration.sourceCommit,
|
|
4275
|
+
mainBranch: integration.mainBranch,
|
|
4276
|
+
mainTipCommit: integration.mainTipCommit,
|
|
4277
|
+
},
|
|
4278
|
+
}
|
|
4279
|
+
);
|
|
4280
|
+
} catch (error) {
|
|
4281
|
+
if (
|
|
4282
|
+
!['PUBLISH_LEASE_EXPIRED', 'PUBLISH_LEASE_INVALID'].includes(
|
|
4283
|
+
error?.code
|
|
4284
|
+
)
|
|
4285
|
+
) {
|
|
4286
|
+
activeCommandScopedPublishLease = active;
|
|
4287
|
+
throw error;
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
clearPublishLease(active.target, active.leaseId);
|
|
4291
|
+
if (baseline) {
|
|
4292
|
+
clearChangeBaseline(
|
|
4293
|
+
active.target,
|
|
4294
|
+
baseline.baselineId || baseline.id
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
4298
|
+
activePublishRequestContext = null;
|
|
4299
|
+
return {
|
|
4300
|
+
managed: true,
|
|
4301
|
+
released: true,
|
|
4302
|
+
retained: false,
|
|
4303
|
+
completion:
|
|
4304
|
+
options.succeeded === false
|
|
4305
|
+
? 'prewrite-failed'
|
|
4306
|
+
: 'direct-publish-completed',
|
|
4307
|
+
integration,
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4087
4311
|
function publishContextRequiredError(label, detail) {
|
|
4088
4312
|
const suffix = detail ? `(${detail})` : '';
|
|
4089
4313
|
const error = new Error(
|
|
@@ -5773,7 +5997,12 @@ async function workspace(args) {
|
|
|
5773
5997
|
flags
|
|
5774
5998
|
);
|
|
5775
5999
|
}
|
|
5776
|
-
runReactSpaFormSchemaPublish(
|
|
6000
|
+
await runReactSpaFormSchemaPublish(
|
|
6001
|
+
profileName,
|
|
6002
|
+
profile,
|
|
6003
|
+
bound.appType,
|
|
6004
|
+
publishOptions
|
|
6005
|
+
);
|
|
5777
6006
|
} else {
|
|
5778
6007
|
fail(
|
|
5779
6008
|
[
|
|
@@ -5794,7 +6023,12 @@ async function workspace(args) {
|
|
|
5794
6023
|
flags
|
|
5795
6024
|
);
|
|
5796
6025
|
}
|
|
5797
|
-
runWorkspacePublish(
|
|
6026
|
+
await runWorkspacePublish(
|
|
6027
|
+
profileName,
|
|
6028
|
+
profile,
|
|
6029
|
+
bound.appType,
|
|
6030
|
+
publishOptions.workspaceArgs
|
|
6031
|
+
);
|
|
5798
6032
|
}
|
|
5799
6033
|
if (publishOptions.includeResources) {
|
|
5800
6034
|
if (publishOptions.dryRun) {
|
|
@@ -5828,6 +6062,7 @@ async function workspace(args) {
|
|
|
5828
6062
|
} else if (!publishOptions.quietResourceSkip) {
|
|
5829
6063
|
print('已跳过 src/resources 发布;如需同时发布资源,传 --resources。');
|
|
5830
6064
|
}
|
|
6065
|
+
await finalizeCommandScopedPublishLease({ succeeded: true });
|
|
5831
6066
|
return;
|
|
5832
6067
|
}
|
|
5833
6068
|
|
|
@@ -6644,6 +6879,12 @@ async function page(args) {
|
|
|
6644
6879
|
},
|
|
6645
6880
|
}
|
|
6646
6881
|
);
|
|
6882
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
6883
|
+
succeeded: true,
|
|
6884
|
+
});
|
|
6885
|
+
if (leaseCompletion && data && typeof data === 'object') {
|
|
6886
|
+
data.publishLeaseLifecycle = leaseCompletion;
|
|
6887
|
+
}
|
|
6647
6888
|
return outputPageResult(data, flags);
|
|
6648
6889
|
}
|
|
6649
6890
|
|
|
@@ -6719,6 +6960,15 @@ async function page(args) {
|
|
|
6719
6960
|
legacyFormUuid: item.legacyFormUuid,
|
|
6720
6961
|
});
|
|
6721
6962
|
}
|
|
6963
|
+
if (!activate) {
|
|
6964
|
+
retainCommandScopedPublishLease('staged-page');
|
|
6965
|
+
}
|
|
6966
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
6967
|
+
succeeded: true,
|
|
6968
|
+
});
|
|
6969
|
+
if (leaseCompletion && data && typeof data === 'object') {
|
|
6970
|
+
data.publishLeaseLifecycle = leaseCompletion;
|
|
6971
|
+
}
|
|
6722
6972
|
return outputPageResult(data, flags);
|
|
6723
6973
|
}
|
|
6724
6974
|
|
|
@@ -10347,6 +10597,8 @@ async function resource(args) {
|
|
|
10347
10597
|
: [];
|
|
10348
10598
|
const stagedChangeId =
|
|
10349
10599
|
flags.change || getStoredChangeBaseline(target)?.changeId;
|
|
10600
|
+
const directPublishSourceRevision =
|
|
10601
|
+
getStoredChangeBaseline(target)?.releaseSourceRevision || null;
|
|
10350
10602
|
if (stagedResources.length > 0 && stagedChangeId) {
|
|
10351
10603
|
result.stagedResourcesFile = recordStagedAppReleaseResources(
|
|
10352
10604
|
stagedChangeId,
|
|
@@ -10366,7 +10618,42 @@ async function resource(args) {
|
|
|
10366
10618
|
target
|
|
10367
10619
|
);
|
|
10368
10620
|
}
|
|
10621
|
+
if (stagedResources.length > 0 || flags['stage-only']) {
|
|
10622
|
+
retainCommandScopedPublishLease('staged-resource');
|
|
10623
|
+
}
|
|
10624
|
+
const leaseCompletion = await telemetry.runPhase(
|
|
10625
|
+
'lease-release',
|
|
10626
|
+
async () =>
|
|
10627
|
+
finalizeCommandScopedPublishLease({
|
|
10628
|
+
succeeded: true,
|
|
10629
|
+
})
|
|
10630
|
+
);
|
|
10631
|
+
if (leaseCompletion) {
|
|
10632
|
+
result.publishLeaseLifecycle = leaseCompletion;
|
|
10633
|
+
}
|
|
10369
10634
|
result.timings = telemetry.stop();
|
|
10635
|
+
if (leaseCompletion?.released && stagedChangeId) {
|
|
10636
|
+
const receipt = writeDirectPublishReceipt(stagedChangeId, {
|
|
10637
|
+
command: 'resource publish',
|
|
10638
|
+
writeAttempted:
|
|
10639
|
+
(result.published || []).some(item => item.action !== 'noop') ||
|
|
10640
|
+
(result.deleted || []).length > 0,
|
|
10641
|
+
selectors: [
|
|
10642
|
+
...(result.published || []).map(
|
|
10643
|
+
item => `${item.kind || 'resource'}:${item.code}`
|
|
10644
|
+
),
|
|
10645
|
+
...(result.deleted || []).map(
|
|
10646
|
+
item => `${item.kind || 'resource'}:${item.code}`
|
|
10647
|
+
),
|
|
10648
|
+
],
|
|
10649
|
+
sourceRevision: directPublishSourceRevision,
|
|
10650
|
+
result: {
|
|
10651
|
+
published: (result.published || []).length,
|
|
10652
|
+
deleted: (result.deleted || []).length,
|
|
10653
|
+
},
|
|
10654
|
+
});
|
|
10655
|
+
result.directPublishReceipt = receipt?.file || null;
|
|
10656
|
+
}
|
|
10370
10657
|
if (flags.json) return writeJson(result);
|
|
10371
10658
|
printResourceResult(result);
|
|
10372
10659
|
} finally {
|
|
@@ -10599,6 +10886,10 @@ async function runtime(args) {
|
|
|
10599
10886
|
target,
|
|
10600
10887
|
flags
|
|
10601
10888
|
);
|
|
10889
|
+
const directChangeId =
|
|
10890
|
+
flags.change || getStoredChangeBaseline(target)?.changeId;
|
|
10891
|
+
const directSourceRevision =
|
|
10892
|
+
getStoredChangeBaseline(target)?.releaseSourceRevision || null;
|
|
10602
10893
|
await runRuntimeChangeBaselinePreflight(config, target);
|
|
10603
10894
|
const activationPreconditions = await resolveRuntimeActivationPreconditions(
|
|
10604
10895
|
config,
|
|
@@ -10623,6 +10914,18 @@ async function runtime(args) {
|
|
|
10623
10914
|
}
|
|
10624
10915
|
);
|
|
10625
10916
|
saveRuntimeReleaseState(target, data);
|
|
10917
|
+
const leaseCompletion = await finalizeCommandScopedPublishLease({
|
|
10918
|
+
succeeded: true,
|
|
10919
|
+
});
|
|
10920
|
+
if (leaseCompletion?.released && directChangeId) {
|
|
10921
|
+
writeDirectPublishReceipt(directChangeId, {
|
|
10922
|
+
command: 'runtime activate',
|
|
10923
|
+
writeAttempted: true,
|
|
10924
|
+
selectors: ['runtime:active'],
|
|
10925
|
+
sourceRevision: directSourceRevision,
|
|
10926
|
+
result: { releaseId: data?.id || releaseId },
|
|
10927
|
+
});
|
|
10928
|
+
}
|
|
10626
10929
|
if (flags.json) return writeJson(data);
|
|
10627
10930
|
print(`runtime release 已激活: ${data.id || releaseId} build=${data.buildId || '-'}`);
|
|
10628
10931
|
return;
|
|
@@ -10666,8 +10969,8 @@ async function runtime(args) {
|
|
|
10666
10969
|
// Keep build after lease acquisition. In direct-storage modes the build
|
|
10667
10970
|
// embeds the provider-specific assetBaseUrl returned by the protected
|
|
10668
10971
|
// upload plan; substituting the gateway URL would change chunk/publicPath
|
|
10669
|
-
// behavior. The
|
|
10670
|
-
//
|
|
10972
|
+
// behavior. The async build keeps the short publish-lease heartbeat alive
|
|
10973
|
+
// instead of extending a crash-prone one-hour lease.
|
|
10671
10974
|
let publishLease = await telemetry.runPhase('lease', async () =>
|
|
10672
10975
|
resolveRuntimePublishLeaseForWrite(config, target, flags)
|
|
10673
10976
|
);
|
|
@@ -10748,19 +11051,8 @@ async function runtime(args) {
|
|
|
10748
11051
|
}
|
|
10749
11052
|
}
|
|
10750
11053
|
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
|
-
);
|
|
11054
|
+
// Runtime builds run in an asynchronous child process so the promotion
|
|
11055
|
+
// heartbeat keeps renewing the short lease while compilation proceeds.
|
|
10764
11056
|
await telemetry.runPhase('build', async () =>
|
|
10765
11057
|
runRuntimeBuild({
|
|
10766
11058
|
buildId,
|
|
@@ -10952,8 +11244,35 @@ async function runtime(args) {
|
|
|
10952
11244
|
traceId,
|
|
10953
11245
|
...(stagedResource ? { stagedResource } : {}),
|
|
10954
11246
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
10955
|
-
timings: telemetry.stop(),
|
|
10956
11247
|
};
|
|
11248
|
+
if (stagedResource) {
|
|
11249
|
+
retainCommandScopedPublishLease('staged-runtime');
|
|
11250
|
+
}
|
|
11251
|
+
const leaseCompletion = await telemetry.runPhase(
|
|
11252
|
+
'lease-release',
|
|
11253
|
+
async () =>
|
|
11254
|
+
finalizeCommandScopedPublishLease({
|
|
11255
|
+
succeeded: true,
|
|
11256
|
+
})
|
|
11257
|
+
);
|
|
11258
|
+
if (leaseCompletion) {
|
|
11259
|
+
result.publishLeaseLifecycle = leaseCompletion;
|
|
11260
|
+
}
|
|
11261
|
+
result.timings = telemetry.stop();
|
|
11262
|
+
if (leaseCompletion?.released && stagedChangeId) {
|
|
11263
|
+
const receipt = writeDirectPublishReceipt(stagedChangeId, {
|
|
11264
|
+
command: 'runtime deploy',
|
|
11265
|
+
writeAttempted: true,
|
|
11266
|
+
selectors: ['runtime:active'],
|
|
11267
|
+
sourceRevision: runtimeLineage.sourceRevision,
|
|
11268
|
+
result: {
|
|
11269
|
+
releaseId: data?.id || null,
|
|
11270
|
+
buildId,
|
|
11271
|
+
activated: true,
|
|
11272
|
+
},
|
|
11273
|
+
});
|
|
11274
|
+
result.directPublishReceipt = receipt?.file || null;
|
|
11275
|
+
}
|
|
10957
11276
|
if (flags.json) return writeJson(result);
|
|
10958
11277
|
print(
|
|
10959
11278
|
[
|
|
@@ -11056,32 +11375,61 @@ async function createRuntimeOssUploadBasePlan(options) {
|
|
|
11056
11375
|
);
|
|
11057
11376
|
}
|
|
11058
11377
|
|
|
11059
|
-
function runRuntimeBuild(options) {
|
|
11378
|
+
async function runRuntimeBuild(options) {
|
|
11060
11379
|
const command = options.command || defaultRuntimeBuildCommand();
|
|
11061
11380
|
if (options.jsonOutput) {
|
|
11062
11381
|
printRuntimeProgress(`构建 React SPA runtime: ${command}`);
|
|
11063
11382
|
} else {
|
|
11064
11383
|
print(`构建 React SPA runtime: ${command}`);
|
|
11065
11384
|
}
|
|
11066
|
-
|
|
11067
|
-
|
|
11068
|
-
|
|
11069
|
-
|
|
11070
|
-
|
|
11071
|
-
|
|
11072
|
-
|
|
11073
|
-
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11385
|
+
await new Promise((resolve, reject) => {
|
|
11386
|
+
let child;
|
|
11387
|
+
try {
|
|
11388
|
+
child = spawn(command, [], {
|
|
11389
|
+
cwd: process.cwd(),
|
|
11390
|
+
shell: true,
|
|
11391
|
+
stdio: options.jsonOutput
|
|
11392
|
+
? ['ignore', 'pipe', 'pipe']
|
|
11393
|
+
: 'inherit',
|
|
11394
|
+
env: {
|
|
11395
|
+
...process.env,
|
|
11396
|
+
OPENXIANGDA_APP_TYPE: options.appType,
|
|
11397
|
+
APP_TYPE: options.appType,
|
|
11398
|
+
OPENXIANGDA_BUILD_ID: options.buildId,
|
|
11399
|
+
OPENXIANGDA_RUNTIME_ASSET_BASE: options.assetBaseUrl,
|
|
11400
|
+
},
|
|
11401
|
+
});
|
|
11402
|
+
} catch (error) {
|
|
11403
|
+
reject(
|
|
11404
|
+
new Error(`runtime build 无法启动: ${error?.message || String(error)}`)
|
|
11405
|
+
);
|
|
11406
|
+
return;
|
|
11407
|
+
}
|
|
11408
|
+
if (options.jsonOutput) {
|
|
11409
|
+
child.stdout?.on('data', chunk => {
|
|
11410
|
+
process.stderr.write(maskText(String(chunk)));
|
|
11411
|
+
});
|
|
11412
|
+
child.stderr?.on('data', chunk => {
|
|
11413
|
+
process.stderr.write(maskText(String(chunk)));
|
|
11414
|
+
});
|
|
11415
|
+
}
|
|
11416
|
+
child.once('error', error => {
|
|
11417
|
+
reject(new Error(`runtime build 无法启动: ${error.message}`));
|
|
11418
|
+
});
|
|
11419
|
+
child.once('close', (code, signal) => {
|
|
11420
|
+
if (code === 0) {
|
|
11421
|
+
resolve();
|
|
11422
|
+
return;
|
|
11423
|
+
}
|
|
11424
|
+
reject(
|
|
11425
|
+
new Error(
|
|
11426
|
+
`runtime build 失败: ${
|
|
11427
|
+
Number.isInteger(code) ? `exit ${code}` : `signal ${signal || '-'}`
|
|
11428
|
+
}`
|
|
11429
|
+
)
|
|
11430
|
+
);
|
|
11431
|
+
});
|
|
11078
11432
|
});
|
|
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
11433
|
}
|
|
11086
11434
|
|
|
11087
11435
|
function assertRuntimeBuildReady(explicitCommand) {
|
|
@@ -23461,6 +23809,32 @@ function publishContextHeadersForRequest(config, profileName, apiPath, options =
|
|
|
23461
23809
|
);
|
|
23462
23810
|
}
|
|
23463
23811
|
|
|
23812
|
+
function isPublishMutationRequest(apiPath, options = {}) {
|
|
23813
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
23814
|
+
if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return false;
|
|
23815
|
+
const normalizedPath = String(apiPath || '').split('?')[0];
|
|
23816
|
+
if (
|
|
23817
|
+
normalizedPath.includes('/publish-lease/') ||
|
|
23818
|
+
normalizedPath.endsWith('/publish-lease/acquire') ||
|
|
23819
|
+
normalizedPath.includes('/change-baselines')
|
|
23820
|
+
) {
|
|
23821
|
+
return false;
|
|
23822
|
+
}
|
|
23823
|
+
if (
|
|
23824
|
+
normalizedPath.endsWith('/oss-upload-plan') &&
|
|
23825
|
+
options.body?.planOnly === true
|
|
23826
|
+
) {
|
|
23827
|
+
return false;
|
|
23828
|
+
}
|
|
23829
|
+
if (
|
|
23830
|
+
normalizedPath.endsWith('/schema-storage-plan') ||
|
|
23831
|
+
normalizedPath.endsWith('/storage-plan')
|
|
23832
|
+
) {
|
|
23833
|
+
return false;
|
|
23834
|
+
}
|
|
23835
|
+
return true;
|
|
23836
|
+
}
|
|
23837
|
+
|
|
23464
23838
|
async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
23465
23839
|
assertReadOnlyHttpRequest(apiPath, options);
|
|
23466
23840
|
const resolved = getProfile(config, profileName);
|
|
@@ -23482,6 +23856,9 @@ async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
|
23482
23856
|
) {
|
|
23483
23857
|
await activePublishLeaseHeartbeat?.heartbeat?.beforeWrite();
|
|
23484
23858
|
}
|
|
23859
|
+
if (isPublishMutationRequest(apiPath, requestOptions)) {
|
|
23860
|
+
markCommandScopedPublishWriteAttempted();
|
|
23861
|
+
}
|
|
23485
23862
|
// Build headers after the lease check: a successful renew may replace the
|
|
23486
23863
|
// live client session/expiry metadata, and auth/transient retries must not
|
|
23487
23864
|
// replay stale publish context.
|
|
@@ -23542,6 +23919,7 @@ async function requestFormWithAuth(config, profileName, apiPath, formDataFactory
|
|
|
23542
23919
|
if (!skipPublishLeaseHeartbeat) {
|
|
23543
23920
|
await activePublishLeaseHeartbeat?.heartbeat?.beforeWrite();
|
|
23544
23921
|
}
|
|
23922
|
+
markCommandScopedPublishWriteAttempted();
|
|
23545
23923
|
const currentRequestOptions = {
|
|
23546
23924
|
...requestOptions,
|
|
23547
23925
|
headers: {
|
|
@@ -23759,7 +24137,7 @@ function normalizeWorkspacePublishOptions(flags) {
|
|
|
23759
24137
|
};
|
|
23760
24138
|
}
|
|
23761
24139
|
|
|
23762
|
-
function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
24140
|
+
async function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
23763
24141
|
const packageFile = path.join(process.cwd(), 'package.json');
|
|
23764
24142
|
if (!fs.existsSync(packageFile)) {
|
|
23765
24143
|
fail('当前目录没有 package.json,无法识别工作区发布脚本');
|
|
@@ -23781,14 +24159,13 @@ function runWorkspacePublish(profileName, profile, appType, publishArgs = []) {
|
|
|
23781
24159
|
? ['run', scriptName, '--', ...publishArgs]
|
|
23782
24160
|
: ['run', scriptName];
|
|
23783
24161
|
const globalEnv = loadGlobalEnv();
|
|
23784
|
-
|
|
24162
|
+
await runWorkspaceChildCommand(command, args, {
|
|
23785
24163
|
cwd: process.cwd(),
|
|
23786
|
-
stdio: 'inherit',
|
|
23787
24164
|
env: buildWorkspacePublishEnv(profileName, profile, appType, globalEnv),
|
|
24165
|
+
failureCode: 'WORKSPACE_PUBLISH_FAILED',
|
|
24166
|
+
label: scriptName,
|
|
24167
|
+
marksPublishWrite: true,
|
|
23788
24168
|
});
|
|
23789
|
-
if (result.status !== 0) {
|
|
23790
|
-
process.exit(result.status || 1);
|
|
23791
|
-
}
|
|
23792
24169
|
}
|
|
23793
24170
|
|
|
23794
24171
|
function isReactSpaWorkspace() {
|
|
@@ -23798,7 +24175,12 @@ function isReactSpaWorkspace() {
|
|
|
23798
24175
|
return /runtimeMode\s*:\s*['"]react-spa['"]/.test(content);
|
|
23799
24176
|
}
|
|
23800
24177
|
|
|
23801
|
-
function runReactSpaFormSchemaPublish(
|
|
24178
|
+
async function runReactSpaFormSchemaPublish(
|
|
24179
|
+
profileName,
|
|
24180
|
+
profile,
|
|
24181
|
+
appType,
|
|
24182
|
+
publishOptions
|
|
24183
|
+
) {
|
|
23802
24184
|
const packageFile = path.join(process.cwd(), 'package.json');
|
|
23803
24185
|
if (!fs.existsSync(packageFile)) {
|
|
23804
24186
|
fail('当前目录没有 package.json,无法识别工作区发布脚本');
|
|
@@ -23818,14 +24200,41 @@ function runReactSpaFormSchemaPublish(profileName, profile, appType, publishOpti
|
|
|
23818
24200
|
`React SPA 表单 schema 发布${publishOptions.dryRun ? ' (dry-run)' : ''}: ${publishOptions.targetForm}`
|
|
23819
24201
|
);
|
|
23820
24202
|
const globalEnv = loadGlobalEnv();
|
|
23821
|
-
|
|
24203
|
+
await runWorkspaceChildCommand(command, args, {
|
|
23822
24204
|
cwd: process.cwd(),
|
|
23823
|
-
stdio: 'inherit',
|
|
23824
24205
|
env: buildWorkspacePublishEnv(profileName, profile, appType, globalEnv),
|
|
24206
|
+
failureCode: 'FORM_SCHEMA_PUBLISH_FAILED',
|
|
24207
|
+
label: `form schema ${publishOptions.targetForm}`,
|
|
24208
|
+
marksPublishWrite: true,
|
|
24209
|
+
});
|
|
24210
|
+
}
|
|
24211
|
+
|
|
24212
|
+
async function runWorkspaceChildCommand(command, args, options = {}) {
|
|
24213
|
+
await new Promise((resolve, reject) => {
|
|
24214
|
+
const child = spawn(command, args, {
|
|
24215
|
+
cwd: options.cwd || process.cwd(),
|
|
24216
|
+
stdio: 'inherit',
|
|
24217
|
+
env: options.env || process.env,
|
|
24218
|
+
});
|
|
24219
|
+
child.once('spawn', () => {
|
|
24220
|
+
if (options.marksPublishWrite) {
|
|
24221
|
+
markCommandScopedPublishWriteAttempted();
|
|
24222
|
+
}
|
|
24223
|
+
});
|
|
24224
|
+
child.once('error', reject);
|
|
24225
|
+
child.once('close', (code, signal) => {
|
|
24226
|
+
if (code === 0) {
|
|
24227
|
+
resolve();
|
|
24228
|
+
return;
|
|
24229
|
+
}
|
|
24230
|
+
const error = new Error(
|
|
24231
|
+
`${options.label || command} 执行失败${signal ? `(signal=${signal})` : `(exit=${code ?? 1})`}`
|
|
24232
|
+
);
|
|
24233
|
+
error.code = options.failureCode || 'WORKSPACE_CHILD_COMMAND_FAILED';
|
|
24234
|
+
error.exitCode = Number.isInteger(code) ? code : 1;
|
|
24235
|
+
reject(error);
|
|
24236
|
+
});
|
|
23825
24237
|
});
|
|
23826
|
-
if (result.status !== 0) {
|
|
23827
|
-
process.exit(result.status || 1);
|
|
23828
|
-
}
|
|
23829
24238
|
}
|
|
23830
24239
|
|
|
23831
24240
|
function buildWorkspacePublishEnv(profileName, profile, appType, globalEnv) {
|
package/lib/publish-lease.js
CHANGED
|
@@ -4,8 +4,8 @@ const path = require('path');
|
|
|
4
4
|
|
|
5
5
|
const { saveProjectState } = require('./config');
|
|
6
6
|
|
|
7
|
-
const DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS =
|
|
8
|
-
const PUBLISH_LEASE_RENEW_WINDOW_SECONDS =
|
|
7
|
+
const DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS = 120;
|
|
8
|
+
const PUBLISH_LEASE_RENEW_WINDOW_SECONDS = 60;
|
|
9
9
|
|
|
10
10
|
function getStoredPublishLease(target) {
|
|
11
11
|
const lease = target?.bound?.promotion?.publishLease;
|
package/lib/task-status.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
function buildTaskStatus(input = {}) {
|
|
2
2
|
const execution = object(input.execution);
|
|
3
|
+
const directPublish = object(input.directPublish);
|
|
3
4
|
const steps = Array.isArray(execution?.steps) ? execution.steps : [];
|
|
4
5
|
const completed = steps.filter(step => step.status === 'completed');
|
|
5
6
|
const failed = steps.find(step => step.status === 'failed') || null;
|
|
@@ -7,12 +8,28 @@ function buildTaskStatus(input = {}) {
|
|
|
7
8
|
steps.find(step => ['running', 'write-started'].includes(step.status)) ||
|
|
8
9
|
null;
|
|
9
10
|
const pending = steps.filter(step => step.status === 'pending');
|
|
10
|
-
const releaseState =
|
|
11
|
+
const releaseState =
|
|
12
|
+
execution?.status ||
|
|
13
|
+
(directPublish?.status === 'completed'
|
|
14
|
+
? 'direct-publish-completed'
|
|
15
|
+
: 'not-started');
|
|
11
16
|
const postCommit = object(input.postCommit);
|
|
12
17
|
const remoteLease = object(input.remoteLease);
|
|
13
18
|
const taskResult = object(input.taskResult);
|
|
14
19
|
const integration = object(input.integration);
|
|
15
20
|
const change = object(input.change);
|
|
21
|
+
const sourceRevision =
|
|
22
|
+
object(input.sourceRevision) ||
|
|
23
|
+
object(directPublish?.sourceRevision) ||
|
|
24
|
+
object(execution?.releaseSourceRevision) ||
|
|
25
|
+
object(execution?.releaseContext?.releaseSourceRevision);
|
|
26
|
+
const changeId =
|
|
27
|
+
input.changeId || change?.id || execution?.changeId || null;
|
|
28
|
+
const leaseBlocked = isRemoteLeaseBlocking({
|
|
29
|
+
remoteLease,
|
|
30
|
+
execution,
|
|
31
|
+
changeId,
|
|
32
|
+
});
|
|
16
33
|
const phase = inferPhase({
|
|
17
34
|
releaseState,
|
|
18
35
|
steps,
|
|
@@ -29,14 +46,19 @@ function buildTaskStatus(input = {}) {
|
|
|
29
46
|
remoteLease,
|
|
30
47
|
integration,
|
|
31
48
|
postCommit,
|
|
49
|
+
leaseBlocked,
|
|
32
50
|
});
|
|
33
51
|
const startedAt =
|
|
34
52
|
execution?.createdAt ||
|
|
53
|
+
directPublish?.completedAt ||
|
|
35
54
|
change?.createdAt ||
|
|
36
55
|
taskResult?.recordedAt ||
|
|
37
56
|
null;
|
|
38
57
|
const endedAt =
|
|
39
58
|
execution?.completedAt ||
|
|
59
|
+
(releaseState === 'direct-publish-completed'
|
|
60
|
+
? directPublish?.completedAt
|
|
61
|
+
: null) ||
|
|
40
62
|
(phase === 'integration-ready' ? taskResult?.recordedAt : null);
|
|
41
63
|
const now = Number.isFinite(Number(input.now))
|
|
42
64
|
? Number(input.now)
|
|
@@ -49,17 +71,18 @@ function buildTaskStatus(input = {}) {
|
|
|
49
71
|
running,
|
|
50
72
|
);
|
|
51
73
|
const wrotePlatform = Boolean(
|
|
52
|
-
|
|
74
|
+
directPublish?.writeAttempted ||
|
|
75
|
+
execution?.writeAttempted ||
|
|
53
76
|
execution?.stagedWriteOccurred ||
|
|
54
77
|
completed.some(step => step.id !== 'lease-and-capture'),
|
|
55
78
|
);
|
|
56
79
|
const healthy =
|
|
57
|
-
|
|
80
|
+
['completed', 'direct-publish-completed'].includes(releaseState) &&
|
|
58
81
|
(!postCommit || postCommit.status === 'completed');
|
|
59
82
|
|
|
60
83
|
return {
|
|
61
84
|
schemaVersion: 'openxiangda_task_status_v1',
|
|
62
|
-
changeId
|
|
85
|
+
changeId,
|
|
63
86
|
phase,
|
|
64
87
|
healthy,
|
|
65
88
|
wrotePlatform,
|
|
@@ -73,8 +96,8 @@ function buildTaskStatus(input = {}) {
|
|
|
73
96
|
},
|
|
74
97
|
source: {
|
|
75
98
|
changeStatus: change?.status || null,
|
|
76
|
-
taskCommit: taskResult?.commit || null,
|
|
77
|
-
taskReady: Boolean(taskResult),
|
|
99
|
+
taskCommit: taskResult?.commit || sourceRevision?.baseCommit || null,
|
|
100
|
+
taskReady: Boolean(taskResult || sourceRevision),
|
|
78
101
|
integrated: inferIntegrated(integration),
|
|
79
102
|
},
|
|
80
103
|
release: {
|
|
@@ -86,6 +109,10 @@ function buildTaskStatus(input = {}) {
|
|
|
86
109
|
remoteLease?.holder?.changeId ||
|
|
87
110
|
remoteLease?.holder?.clientSessionId ||
|
|
88
111
|
remoteLease?.changeId ||
|
|
112
|
+
remoteLease?.clientSessionId ||
|
|
113
|
+
(typeof remoteLease?.holder === 'string'
|
|
114
|
+
? remoteLease.holder
|
|
115
|
+
: null) ||
|
|
89
116
|
null,
|
|
90
117
|
},
|
|
91
118
|
blocker: blockingLayer
|
|
@@ -100,7 +127,7 @@ function buildTaskStatus(input = {}) {
|
|
|
100
127
|
message:
|
|
101
128
|
failed?.error?.message ||
|
|
102
129
|
failed?.error ||
|
|
103
|
-
blockerMessage(blockingLayer),
|
|
130
|
+
blockerMessage(blockingLayer, remoteLease),
|
|
104
131
|
}
|
|
105
132
|
: null,
|
|
106
133
|
nextAction: nextAction({
|
|
@@ -111,6 +138,7 @@ function buildTaskStatus(input = {}) {
|
|
|
111
138
|
postCommit,
|
|
112
139
|
taskResult,
|
|
113
140
|
integration,
|
|
141
|
+
leaseBlocked,
|
|
114
142
|
}),
|
|
115
143
|
};
|
|
116
144
|
}
|
|
@@ -124,6 +152,7 @@ function inferPhase(input) {
|
|
|
124
152
|
return 'health';
|
|
125
153
|
}
|
|
126
154
|
if (input.releaseState === 'completed') return 'healthy';
|
|
155
|
+
if (input.releaseState === 'direct-publish-completed') return 'healthy';
|
|
127
156
|
if (input.releaseState === 'write-review-required') return 'write-review';
|
|
128
157
|
if (input.failed) return 'failed';
|
|
129
158
|
if (input.running) return phaseFromStep(input.running.id);
|
|
@@ -163,13 +192,7 @@ function inferBlockingLayer(input) {
|
|
|
163
192
|
if (input.postCommit?.status && input.postCommit.status !== 'completed') {
|
|
164
193
|
return 'post-commit';
|
|
165
194
|
}
|
|
166
|
-
if (
|
|
167
|
-
input.remoteLease?.active &&
|
|
168
|
-
input.releaseState !== 'running' &&
|
|
169
|
-
input.releaseState !== 'staged-resumable'
|
|
170
|
-
) {
|
|
171
|
-
return 'lease';
|
|
172
|
-
}
|
|
195
|
+
if (input.leaseBlocked) return 'lease';
|
|
173
196
|
if (input.integration && inferIntegrated(input.integration) === false) {
|
|
174
197
|
return 'git-mainline';
|
|
175
198
|
}
|
|
@@ -231,13 +254,26 @@ function nextAction(input) {
|
|
|
231
254
|
if (input.failed) {
|
|
232
255
|
return `修复 ${input.failed.id || '失败步骤'} 后重跑同一条 release publish。`;
|
|
233
256
|
}
|
|
234
|
-
if (input.
|
|
235
|
-
|
|
257
|
+
if (input.leaseBlocked) {
|
|
258
|
+
const holder =
|
|
259
|
+
input.remoteLease?.changeId ||
|
|
260
|
+
input.remoteLease?.clientSessionId ||
|
|
261
|
+
input.remoteLease?.holder?.changeId ||
|
|
262
|
+
input.remoteLease?.holder?.clientSessionId ||
|
|
263
|
+
input.remoteLease?.holder ||
|
|
264
|
+
'另一发布任务';
|
|
265
|
+
const expiry = input.remoteLease?.expiresAt
|
|
266
|
+
? `,最晚 ${input.remoteLease.expiresAt} 到期`
|
|
267
|
+
: '';
|
|
268
|
+
return `等待 ${holder} 释放当前应用租约${expiry};本任务尚未写平台,可继续本地开发与测试。`;
|
|
236
269
|
}
|
|
237
270
|
if (input.releaseState === 'staged-resumable') {
|
|
238
271
|
return '重跑同一条 release publish,从已验证的 staged child 继续。';
|
|
239
272
|
}
|
|
240
273
|
if (input.releaseState === 'completed') return '执行业务验收并归档 change。';
|
|
274
|
+
if (input.releaseState === 'direct-publish-completed') {
|
|
275
|
+
return '直接发布已完成;执行业务验收并归档 change。';
|
|
276
|
+
}
|
|
241
277
|
if (input.taskResult && inferIntegrated(input.integration) === false) {
|
|
242
278
|
return '把 task commit 合并并推送到权威主分支,再创建 mainline bundle。';
|
|
243
279
|
}
|
|
@@ -245,9 +281,11 @@ function nextAction(input) {
|
|
|
245
281
|
return '完成实现和聚焦测试,提交后运行 sdd ready。';
|
|
246
282
|
}
|
|
247
283
|
|
|
248
|
-
function blockerMessage(layer) {
|
|
284
|
+
function blockerMessage(layer, remoteLease) {
|
|
249
285
|
const messages = {
|
|
250
|
-
lease:
|
|
286
|
+
lease: remoteLease?.changeId
|
|
287
|
+
? `发布任务 ${remoteLease.changeId} 持有应用租约。`
|
|
288
|
+
: '另一个发布任务持有应用租约。',
|
|
251
289
|
'git-mainline': '任务提交尚未完整进入权威主分支。',
|
|
252
290
|
'post-commit': '发布已激活,提交后动作仍在自动重试。',
|
|
253
291
|
'write-result': '上次写请求结果不确定,需要只读核对。',
|
|
@@ -255,6 +293,31 @@ function blockerMessage(layer) {
|
|
|
255
293
|
return messages[layer] || '当前阶段需要处理失败后才能继续。';
|
|
256
294
|
}
|
|
257
295
|
|
|
296
|
+
function isRemoteLeaseBlocking(input = {}) {
|
|
297
|
+
const remote = object(input.remoteLease);
|
|
298
|
+
if (!remote?.active) return false;
|
|
299
|
+
const execution = object(input.execution);
|
|
300
|
+
const context = object(execution?.releaseContext);
|
|
301
|
+
if (
|
|
302
|
+
remote.leaseId &&
|
|
303
|
+
context?.leaseId &&
|
|
304
|
+
String(remote.leaseId) === String(context.leaseId)
|
|
305
|
+
) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
remote.clientSessionId &&
|
|
310
|
+
context?.clientSessionId &&
|
|
311
|
+
String(remote.clientSessionId) === String(context.clientSessionId) &&
|
|
312
|
+
(!remote.changeId ||
|
|
313
|
+
!input.changeId ||
|
|
314
|
+
String(remote.changeId) === String(input.changeId))
|
|
315
|
+
) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
258
321
|
function object(value) {
|
|
259
322
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
260
323
|
? value
|
|
@@ -10,6 +10,13 @@ OpenXiangda connects an AI coding workspace to the private low-code platform thr
|
|
|
10
10
|
|
|
11
11
|
This file is a router and safety card. Read only the one or two subskills selected below; do not load every OpenXiangda reference into the same turn.
|
|
12
12
|
|
|
13
|
+
## Keep the agent loop small
|
|
14
|
+
|
|
15
|
+
- Start with one broad CodeGraph exploration that names the complete flow or feature. Use at most one focused follow-up when the first result explicitly omits a required symbol; do not repeat overlapping surveys.
|
|
16
|
+
- Ordinary work uses this router plus one domain subskill. Add a second domain subskill only when the accepted scope genuinely crosses domains; three or more OpenXiangda subskills require stopping and narrowing the task.
|
|
17
|
+
- When the user already supplied concrete requirements and acceptance criteria, record the structured SDD scope and implement. Do not turn a resolved request into a long design essay or ask for a duplicate confirmation.
|
|
18
|
+
- During an unchanged lease/build wait, report the first blocker and then only material state changes. Prefer `openxiangda task status --watch` over repeated narrative updates.
|
|
19
|
+
|
|
13
20
|
## Decide the track
|
|
14
21
|
|
|
15
22
|
| Intent | Read next | First action |
|
|
@@ -27,7 +34,7 @@ This file is a router and safety card. Read only the one or two subskills select
|
|
|
27
34
|
## Scope before work
|
|
28
35
|
|
|
29
36
|
1. Work from the app workspace root and pass `--profile <name>` to every write or release command.
|
|
30
|
-
2. In a shared
|
|
37
|
+
2. In a shared app repository, reserve the canonical `main`/`master` checkout for integration and release. Every development task uses an isolated Git worktree/branch from the latest remote mainline; never stash, restore, or overwrite another task's changes to make the canonical checkout publishable. Merge approved task commits into the authoritative remote default branch before any live release. Formal publish runs once from a clean local `main`/`master` that exactly equals the remote tip; feature worktrees never publish.
|
|
31
38
|
3. Give the task one stable SDD change id. Use `--change <id>` on context, plan, check, verify, publish, and archive commands when supported.
|
|
32
39
|
4. Plan and publish exact resource codes with `--only <codes>` or `--code <code>`; a type-only or full-resource publish is allowed only when the dependency closure intentionally contains the whole type/application.
|
|
33
40
|
5. A React SPA page change rebuilds one application runtime. Commit the complete build input first, upload/preview with `runtime deploy --no-activate`, and promote only from a clean merged release head that descends from the online Runtime source revision.
|
|
@@ -24,7 +24,9 @@ openxiangda sdd context --change <change> --changed --json
|
|
|
24
24
|
|
|
25
25
|
Run `openxiangda update check --json` once per substantial task, on a suspected mismatch, or when the cached check is older than one day. If an update is installed, refresh skills once with `openxiangda skill install --force`.
|
|
26
26
|
|
|
27
|
-
Every write/release command must include `--profile <name>`.
|
|
27
|
+
Every write/release command must include `--profile <name>`. In a shared app repository the canonical main checkout is integration/release-only; parallel tasks develop in isolated Git worktrees/branches and never stash or restore another task's files. Merge approved commits into the authoritative remote default branch, create one `sdd bundle` for the intended changes, commit/push it, and publish once from a clean local `main`/`master` that exactly equals the remote tip.
|
|
28
|
+
|
|
29
|
+
Keep investigation proportional: one complete CodeGraph survey plus at most one focused follow-up, normally one domain subskill, and no optional prose for a request whose requirements and acceptance criteria are already explicit. Unchanged lease waits should use `task status --watch` and emit only material transitions.
|
|
28
30
|
|
|
29
31
|
SDD is structured-first by default: only `change.json`, `coverage.json`, and `release.json` are created. Generate optional prose with `openxiangda sdd render <change>`; missing checklist/evidence/spec prose is a warning, while approval, exact structured scope, actual argv, mainline identity, child CAS, lease, and atomic activation remain hard gates. Set `strictDocumentation: true` only when prose completion must intentionally block a workspace.
|
|
30
32
|
|
package/package.json
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
## 开发原则
|
|
7
7
|
|
|
8
|
-
-
|
|
8
|
+
- 架构类需求先运行 `openxiangda doctor --json` 和 `openxiangda design gates --topic <code> --json`。只有仍存在会改变实现方向的业务、安全或数据选择时才输出设计并等待确认;用户已经给出具体需求与验收标准时,直接记录结构化 SDD 范围并实现,不再写长篇设计或重复确认。
|
|
9
9
|
- 先按风险分级:只读/文档/测试为 L0;纯文案样式或单一既有资源绑定等可逆窄改为 L1,可用受限的 `openxiangda sdd quick` 记录精确范围;表单结构、业务函数、自动化/流程、权限、登录/公开访问、数据写入和 runtime/config 为 L2;不可逆、生产迁移或应用级扩权为 L3。L2/L3 必须挂完整 SDD,并由 `coverage.json` 把需求与场景映射到精确的文件、表单、页面及工程资源范围。
|
|
10
10
|
- 一个开发任务只使用一个显式 change,并从 `openxiangda sdd context --change <change> --changed --json` 开始;默认只维护 change/coverage/release 三份结构化事实源,需要文档时才执行 `openxiangda sdd render <change>`。结构化 approval、资源和文件范围是硬约束,任务/证据/规格文案默认只告警。只有明确配置 `strictDocumentation: true` 才把文案完成度恢复为门禁。
|
|
11
|
-
-
|
|
11
|
+
- 多会话开发把规范的 `main`/`master` 检出专用于集成和发布;每个开发任务从最新远端主线使用独立 Git worktree/branch,禁止为了发布 stash/restore 或覆盖其他任务的文件。feature worktree 不发布。把已批准提交合并并 push 到权威默认主分支后,用 `sdd bundle <release-change> --changes ...` 聚合范围;只从与远端 tip 完全一致的 clean `main`/`master` 一次发布。
|
|
12
|
+
- 调查预算默认为一次覆盖完整调用链的 CodeGraph 查询,只有明确缺失符号时再补一次精确查询;普通任务只加载一个领域技能。租约、构建或部署状态未变化时不重复输出相同进度,使用 `openxiangda task status --watch` 等待状态变化。
|
|
12
13
|
- 账号、角色、权限、数据范围、组织账号、RBAC、查询参数授权需求必须先运行 `openxiangda design gates --topic permissions --json`,选择 `managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,输出权限矩阵后再实现。
|
|
13
14
|
- 应用角色只读查询组织账号时声明 `app:organization:read`;创建、修改账号/部门或重置密码时声明 `app:organization:manage`。创建角色、分配成员或维护权限组也必须在角色资源的 `apiPermissionCodes` 声明对应的 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`。
|
|
14
15
|
- 默认用户界面保持克制:左侧应用导航、顶部账号信息、首页内容区域。
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
**所有"发布 / 上线 / 部署 / publish / deploy / ship / release"请求,唯一正确入口是 `openxiangda workspace publish --profile <name>`,不要直接 `pnpm publish:all`。**
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**架构类需求先过设计门。** 新应用、复杂页面、登录注册、公开访问、权限数据范围、流程自动化、连接器/通知等需求,先 `openxiangda doctor --json` + `openxiangda design gates --topic <code> --json`。只有仍存在会改变实现方向的业务、安全或数据选择时才输出设计并等待确认;用户已经给出具体需求与验收标准时,直接记录结构化 SDD 范围并实现,不再写长篇设计或重复确认。
|
|
12
|
+
|
|
13
|
+
共享应用仓库把规范 `main`/`master` 检出专用于集成和发布;每个开发任务从最新远端主线使用独立 Git worktree/branch,禁止为了发布 stash/restore 或覆盖其他任务文件。调查默认只做一次完整 CodeGraph 查询,明确缺失时最多补一次精确查询;普通任务只加载一个领域技能。等待状态未变化时使用 `openxiangda task status --watch`,不要重复输出相同进度。
|
|
12
14
|
|
|
13
15
|
**按风险分级治理。** 只读/文档/测试为 L0;纯文案样式或单一既有资源绑定等窄小可逆改动为 L1,可用受限的 `openxiangda sdd quick` 记录精确范围;表单结构、业务 Function、Automation/Workflow、权限、登录/公开访问、数据写入和 runtime/config 为 L2;不可逆、生产迁移或应用级扩权为 L3。L2/L3 必须挂完整 SDD,并由 `coverage.json` 把需求与场景映射到精确的文件、表单、页面及工程资源范围。
|
|
14
16
|
|