openxiangda 1.0.173 → 1.0.175
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 +2 -0
- package/lib/cli.js +407 -50
- package/lib/release-mainline.js +13 -0
- package/lib/sdd.js +68 -15
- package/openxiangda-skills/SKILL.md +4 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ Architecture-class requests are plan-gated by default. For new apps, complex pag
|
|
|
94
94
|
|
|
95
95
|
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 streamlined by default: incomplete task/evidence/spec prose warns but does not block. 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.
|
|
96
96
|
|
|
97
|
+
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.
|
|
98
|
+
|
|
97
99
|
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.
|
|
98
100
|
|
|
99
101
|
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.
|
package/lib/cli.js
CHANGED
|
@@ -949,6 +949,60 @@ function summarizeReleaseStepResult(result) {
|
|
|
949
949
|
};
|
|
950
950
|
}
|
|
951
951
|
|
|
952
|
+
function releaseExecutionContextFromBegin(result, target, changeId) {
|
|
953
|
+
const value =
|
|
954
|
+
result && typeof result === 'object' && !Array.isArray(result)
|
|
955
|
+
? result
|
|
956
|
+
: {};
|
|
957
|
+
const releaseSourceRevision =
|
|
958
|
+
value.releaseSourceRevision &&
|
|
959
|
+
typeof value.releaseSourceRevision === 'object' &&
|
|
960
|
+
!Array.isArray(value.releaseSourceRevision)
|
|
961
|
+
? value.releaseSourceRevision
|
|
962
|
+
: null;
|
|
963
|
+
if (
|
|
964
|
+
!value.leaseId ||
|
|
965
|
+
!value.clientSessionId ||
|
|
966
|
+
!releaseSourceRevision?.baseCommit
|
|
967
|
+
) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
appType: target.appType,
|
|
972
|
+
profile: target.profileName,
|
|
973
|
+
changeId,
|
|
974
|
+
leaseId: String(value.leaseId),
|
|
975
|
+
baselineId: value.baselineId ? String(value.baselineId) : null,
|
|
976
|
+
clientSessionId: String(value.clientSessionId),
|
|
977
|
+
releaseSourceRevision,
|
|
978
|
+
capturedAt: new Date().toISOString(),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function isDefinitelyPreWriteReleaseError(error) {
|
|
983
|
+
const code = String(error?.code || '');
|
|
984
|
+
if (new Set([
|
|
985
|
+
'RELEASE_PUBLISH_REVISION_CHANGED',
|
|
986
|
+
'RELEASE_SOURCE_DIRTY',
|
|
987
|
+
'RELEASE_SOURCE_BRANCH_REQUIRED',
|
|
988
|
+
'RELEASE_SOURCE_MAINLINE_REQUIRED',
|
|
989
|
+
'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
|
|
990
|
+
'RELEASE_SOURCE_BEHIND_MAIN',
|
|
991
|
+
'RELEASE_GIT_AUTH_REQUIRED',
|
|
992
|
+
'RELEASE_MAIN_BRANCH_UNRESOLVED',
|
|
993
|
+
'RELEASE_MAIN_REF_UNVERIFIED',
|
|
994
|
+
'SDD_PREPUBLISH_FAILED',
|
|
995
|
+
'PUBLISH_CONTEXT_REQUIRED',
|
|
996
|
+
'APP_RELEASE_STAGED_SCOPE_REQUIRED',
|
|
997
|
+
'APP_RELEASE_STAGED_SCOPE_MISMATCH',
|
|
998
|
+
]).has(code)) {
|
|
999
|
+
return true;
|
|
1000
|
+
}
|
|
1001
|
+
return /^(?:SDD_|RELEASE_(?:SOURCE|GIT|MAINLINE|MAIN_BRANCH|MAIN_REF)|RUNTIME_(?:SOURCE|BUILD|PACKAGE|DEPENDENCY))/.test(
|
|
1002
|
+
code
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
952
1006
|
function parseChildJsonOutput(stdout) {
|
|
953
1007
|
const text = String(stdout || '').trim();
|
|
954
1008
|
if (!text) return null;
|
|
@@ -1061,6 +1115,138 @@ async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
|
1061
1115
|
}
|
|
1062
1116
|
}
|
|
1063
1117
|
|
|
1118
|
+
function readReleaseRecoveryContext(target, changeId) {
|
|
1119
|
+
if (!changeId) return null;
|
|
1120
|
+
const execution = readReleaseExecution(changeId);
|
|
1121
|
+
const context = execution?.releaseContext;
|
|
1122
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
if (
|
|
1126
|
+
context.appType !== target.appType ||
|
|
1127
|
+
context.profile !== target.profileName ||
|
|
1128
|
+
context.changeId !== changeId ||
|
|
1129
|
+
!context.leaseId ||
|
|
1130
|
+
!context.clientSessionId ||
|
|
1131
|
+
!context.releaseSourceRevision
|
|
1132
|
+
) {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
return { execution, context };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function remoteLeaseOwnedByRecovery(remote, recovery) {
|
|
1139
|
+
if (!remote?.active || !recovery) return false;
|
|
1140
|
+
const holder = remote.holder;
|
|
1141
|
+
const holderIsSelf =
|
|
1142
|
+
holder === 'self' ||
|
|
1143
|
+
holder?.self === true ||
|
|
1144
|
+
holder?.clientSessionId === recovery.clientSessionId;
|
|
1145
|
+
return Boolean(
|
|
1146
|
+
holderIsSelf &&
|
|
1147
|
+
String(remote.leaseId || '') === String(recovery.leaseId) &&
|
|
1148
|
+
String(remote.changeId || '') === String(recovery.changeId)
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
async function endReleaseWithoutLocalLease(
|
|
1153
|
+
config,
|
|
1154
|
+
target,
|
|
1155
|
+
flags,
|
|
1156
|
+
baseline
|
|
1157
|
+
) {
|
|
1158
|
+
const changeId = readStringFlag(flags, 'change');
|
|
1159
|
+
const recovered = readReleaseRecoveryContext(target, changeId);
|
|
1160
|
+
const recovery = recovered?.context || null;
|
|
1161
|
+
const clientSessionId =
|
|
1162
|
+
recovery?.clientSessionId ||
|
|
1163
|
+
baseline?.clientSessionId ||
|
|
1164
|
+
createPublishClientSessionId();
|
|
1165
|
+
const remote = await fetchPublishLeaseStatus(
|
|
1166
|
+
config,
|
|
1167
|
+
target,
|
|
1168
|
+
clientSessionId
|
|
1169
|
+
);
|
|
1170
|
+
const sourceRevision =
|
|
1171
|
+
baseline?.releaseSourceRevision ||
|
|
1172
|
+
recovery?.releaseSourceRevision ||
|
|
1173
|
+
null;
|
|
1174
|
+
|
|
1175
|
+
if (remote?.active) {
|
|
1176
|
+
if (!recovery) {
|
|
1177
|
+
fail(
|
|
1178
|
+
`PUBLISH_LEASE_RECOVERY_REQUIRED: 远端仍有活动发布租约 ${remote.leaseId || '-'};本地状态缺失,禁止误报 inactive。请使用原 change 的私有 execution.json 恢复,或等待租约过期后只读核对`
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
if (!remoteLeaseOwnedByRecovery(remote, recovery)) {
|
|
1182
|
+
fail(
|
|
1183
|
+
'PUBLISH_LEASE_RECOVERY_MISMATCH: 远端活动租约不属于当前 change/session;未释放任何租约'
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const integration = assertReleaseSourceIntegrated(sourceRevision, {
|
|
1187
|
+
cwd: process.cwd(),
|
|
1188
|
+
});
|
|
1189
|
+
const remoteResult = await requestWithAuth(
|
|
1190
|
+
config,
|
|
1191
|
+
target.profileName,
|
|
1192
|
+
publishLeaseApiPath(
|
|
1193
|
+
target,
|
|
1194
|
+
`/${encodeURIComponent(recovery.leaseId)}/release`
|
|
1195
|
+
),
|
|
1196
|
+
{
|
|
1197
|
+
method: 'POST',
|
|
1198
|
+
body: {
|
|
1199
|
+
completion: 'mainline-integrated-recovered',
|
|
1200
|
+
sourceCommit: integration.sourceCommit,
|
|
1201
|
+
mainBranch: integration.mainBranch,
|
|
1202
|
+
mainTipCommit: integration.mainTipCommit,
|
|
1203
|
+
},
|
|
1204
|
+
}
|
|
1205
|
+
);
|
|
1206
|
+
if (baseline) {
|
|
1207
|
+
clearChangeBaseline(target, baseline.baselineId || baseline.id);
|
|
1208
|
+
}
|
|
1209
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1210
|
+
recovered.execution.leaseReleasedAt = new Date().toISOString();
|
|
1211
|
+
recovered.execution.leaseReleaseRecovered = true;
|
|
1212
|
+
recovered.execution.updatedAt = recovered.execution.leaseReleasedAt;
|
|
1213
|
+
writePrivateJsonAtomic(
|
|
1214
|
+
releaseExecutionFile(changeId),
|
|
1215
|
+
recovered.execution
|
|
1216
|
+
);
|
|
1217
|
+
return {
|
|
1218
|
+
...(remoteResult && typeof remoteResult === 'object'
|
|
1219
|
+
? remoteResult
|
|
1220
|
+
: {}),
|
|
1221
|
+
active: false,
|
|
1222
|
+
appType: target.appType,
|
|
1223
|
+
recovered: true,
|
|
1224
|
+
integration,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
const integration = sourceRevision
|
|
1229
|
+
? assertReleaseSourceIntegrated(sourceRevision, { cwd: process.cwd() })
|
|
1230
|
+
: null;
|
|
1231
|
+
if (baseline && !integration) {
|
|
1232
|
+
fail(
|
|
1233
|
+
'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
if (baseline) {
|
|
1237
|
+
clearChangeBaseline(target, baseline.baselineId || baseline.id);
|
|
1238
|
+
}
|
|
1239
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1240
|
+
return {
|
|
1241
|
+
active: false,
|
|
1242
|
+
appType: target.appType,
|
|
1243
|
+
local: false,
|
|
1244
|
+
remote,
|
|
1245
|
+
recovered: false,
|
|
1246
|
+
integration,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1064
1250
|
async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
1065
1251
|
const changeId = readStringFlag(flags, 'change');
|
|
1066
1252
|
if (!changeId) {
|
|
@@ -1239,13 +1425,6 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1239
1425
|
}
|
|
1240
1426
|
journalStep.status = 'running';
|
|
1241
1427
|
journalStep.startedAt = new Date().toISOString();
|
|
1242
|
-
if (
|
|
1243
|
-
step.id !== 'lease-and-capture' &&
|
|
1244
|
-
step.id !== 'release-lease' &&
|
|
1245
|
-
step.writes !== false
|
|
1246
|
-
) {
|
|
1247
|
-
execution.writeAttempted = true;
|
|
1248
|
-
}
|
|
1249
1428
|
execution.updatedAt = journalStep.startedAt;
|
|
1250
1429
|
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1251
1430
|
warn(`发布步骤开始: ${step.id}`);
|
|
@@ -1253,6 +1432,20 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1253
1432
|
journalStep.status = 'completed';
|
|
1254
1433
|
journalStep.completedAt = new Date().toISOString();
|
|
1255
1434
|
journalStep.result = summarizeReleaseStepResult(result);
|
|
1435
|
+
if (step.id === 'lease-and-capture') {
|
|
1436
|
+
execution.releaseContext = releaseExecutionContextFromBegin(
|
|
1437
|
+
result,
|
|
1438
|
+
target,
|
|
1439
|
+
changeId
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
if (
|
|
1443
|
+
step.id !== 'lease-and-capture' &&
|
|
1444
|
+
step.id !== 'release-lease' &&
|
|
1445
|
+
step.writes !== false
|
|
1446
|
+
) {
|
|
1447
|
+
execution.writeAttempted = true;
|
|
1448
|
+
}
|
|
1256
1449
|
if (step.stagedKind) execution.stagedWriteOccurred = true;
|
|
1257
1450
|
execution.updatedAt = journalStep.completedAt;
|
|
1258
1451
|
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
@@ -1282,6 +1475,18 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1282
1475
|
code: error.code || 'RELEASE_STEP_FAILED',
|
|
1283
1476
|
message: maskText(error.message).slice(0, 1000),
|
|
1284
1477
|
};
|
|
1478
|
+
const runningDefinition = runnable.find(
|
|
1479
|
+
step => step.id === runningStep.id
|
|
1480
|
+
);
|
|
1481
|
+
if (
|
|
1482
|
+
runningDefinition &&
|
|
1483
|
+
runningDefinition.id !== 'lease-and-capture' &&
|
|
1484
|
+
runningDefinition.id !== 'release-lease' &&
|
|
1485
|
+
runningDefinition.writes !== false &&
|
|
1486
|
+
!isDefinitelyPreWriteReleaseError(error)
|
|
1487
|
+
) {
|
|
1488
|
+
execution.writeAttempted = true;
|
|
1489
|
+
}
|
|
1285
1490
|
}
|
|
1286
1491
|
execution.status = execution.stagedWriteOccurred
|
|
1287
1492
|
? 'staged-resumable'
|
|
@@ -1332,6 +1537,7 @@ async function release(args) {
|
|
|
1332
1537
|
' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
|
|
1333
1538
|
'说明:',
|
|
1334
1539
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
1540
|
+
' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
|
|
1335
1541
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
1336
1542
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
1337
1543
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
@@ -1417,10 +1623,14 @@ async function release(args) {
|
|
|
1417
1623
|
const baseline = getStoredChangeBaseline(target, {
|
|
1418
1624
|
access: 'reconciliation-read',
|
|
1419
1625
|
});
|
|
1420
|
-
const
|
|
1626
|
+
const changeId = positional[0] || readStringFlag(flags, 'change');
|
|
1627
|
+
const recovered = readReleaseRecoveryContext(target, changeId);
|
|
1628
|
+
const sourceRevision =
|
|
1629
|
+
baseline?.releaseSourceRevision ||
|
|
1630
|
+
recovered?.context?.releaseSourceRevision;
|
|
1421
1631
|
if (!sourceRevision) {
|
|
1422
1632
|
fail(
|
|
1423
|
-
'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision
|
|
1633
|
+
'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision;发布已结束时请同时传入 --change <id>,以便从私有执行日志恢复主线证据'
|
|
1424
1634
|
);
|
|
1425
1635
|
}
|
|
1426
1636
|
const result = inspectReleaseSourceIntegration(
|
|
@@ -1599,34 +1809,22 @@ async function release(args) {
|
|
|
1599
1809
|
return;
|
|
1600
1810
|
}
|
|
1601
1811
|
if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
|
|
1602
|
-
assertOrClaimWorktreeOwner({
|
|
1603
|
-
cwd: process.cwd(),
|
|
1604
|
-
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1605
|
-
});
|
|
1606
1812
|
const leaseId = leaseIdFromArgs || stored?.leaseId;
|
|
1607
1813
|
if (!leaseId) {
|
|
1608
|
-
const
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
fail(
|
|
1615
|
-
'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
|
|
1616
|
-
);
|
|
1617
|
-
}
|
|
1618
|
-
const result = {
|
|
1619
|
-
active: false,
|
|
1620
|
-
appType: target.appType,
|
|
1621
|
-
local: false,
|
|
1622
|
-
integration,
|
|
1623
|
-
};
|
|
1624
|
-
if (baseline) clearChangeBaseline(target, baseline.baselineId);
|
|
1625
|
-
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1814
|
+
const result = await endReleaseWithoutLocalLease(
|
|
1815
|
+
config,
|
|
1816
|
+
target,
|
|
1817
|
+
flags,
|
|
1818
|
+
baseline
|
|
1819
|
+
);
|
|
1626
1820
|
if (flags.json) return writeJson(result);
|
|
1627
1821
|
print(`当前工作区没有活动发布租约: ${target.appType}`);
|
|
1628
1822
|
return;
|
|
1629
1823
|
}
|
|
1824
|
+
assertOrClaimWorktreeOwner({
|
|
1825
|
+
cwd: process.cwd(),
|
|
1826
|
+
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1827
|
+
});
|
|
1630
1828
|
const integration = assertReleaseSourceIntegrated(
|
|
1631
1829
|
baseline?.releaseSourceRevision,
|
|
1632
1830
|
{ cwd: process.cwd() }
|
|
@@ -2427,8 +2625,15 @@ function changedAppReleaseResources(detailResources, captureResources) {
|
|
|
2427
2625
|
}
|
|
2428
2626
|
|
|
2429
2627
|
function assertAppReleaseActualGate(target, flags, scope, command) {
|
|
2628
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(
|
|
2629
|
+
readWorkspaceConfigText()
|
|
2630
|
+
);
|
|
2631
|
+
const normalized = normalizeReleaseTargets(
|
|
2632
|
+
scope.targets || {},
|
|
2633
|
+
runtimeMode
|
|
2634
|
+
);
|
|
2430
2635
|
assertSddReleaseGate(
|
|
2431
|
-
buildExplicitSddReleasePlan(
|
|
2636
|
+
buildExplicitSddReleasePlan(normalized.activationTargets, {
|
|
2432
2637
|
files: scope.changedFiles || [],
|
|
2433
2638
|
commands: [command],
|
|
2434
2639
|
changeId: scope.changeId,
|
|
@@ -2596,6 +2801,70 @@ async function activateAppReleaseDetail(
|
|
|
2596
2801
|
);
|
|
2597
2802
|
}
|
|
2598
2803
|
|
|
2804
|
+
function summarizeAppReleaseHead(data) {
|
|
2805
|
+
const value =
|
|
2806
|
+
data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
2807
|
+
const release =
|
|
2808
|
+
value.release &&
|
|
2809
|
+
typeof value.release === 'object' &&
|
|
2810
|
+
!Array.isArray(value.release)
|
|
2811
|
+
? value.release
|
|
2812
|
+
: {};
|
|
2813
|
+
const manifest =
|
|
2814
|
+
release.manifestJson &&
|
|
2815
|
+
typeof release.manifestJson === 'object'
|
|
2816
|
+
? release.manifestJson
|
|
2817
|
+
: value.manifestJson &&
|
|
2818
|
+
typeof value.manifestJson === 'object'
|
|
2819
|
+
? value.manifestJson
|
|
2820
|
+
: {};
|
|
2821
|
+
const resources = Array.isArray(manifest.resources)
|
|
2822
|
+
? manifest.resources
|
|
2823
|
+
: Array.isArray(value.resources)
|
|
2824
|
+
? value.resources
|
|
2825
|
+
: [];
|
|
2826
|
+
const resourcesByKind = Object.fromEntries(
|
|
2827
|
+
Array.from(
|
|
2828
|
+
resources.reduce((counts, resource) => {
|
|
2829
|
+
const kind = String(resource?.kind || 'Unknown');
|
|
2830
|
+
counts.set(kind, (counts.get(kind) || 0) + 1);
|
|
2831
|
+
return counts;
|
|
2832
|
+
}, new Map())
|
|
2833
|
+
).sort(([left], [right]) => left.localeCompare(right))
|
|
2834
|
+
);
|
|
2835
|
+
const compactRelease = {};
|
|
2836
|
+
for (const key of [
|
|
2837
|
+
'id',
|
|
2838
|
+
'status',
|
|
2839
|
+
'parentReleaseId',
|
|
2840
|
+
'manifestHash',
|
|
2841
|
+
'verificationHash',
|
|
2842
|
+
'protocolVersion',
|
|
2843
|
+
'createdAt',
|
|
2844
|
+
'updatedAt',
|
|
2845
|
+
'activatedAt',
|
|
2846
|
+
]) {
|
|
2847
|
+
if (release[key] !== undefined) compactRelease[key] = release[key];
|
|
2848
|
+
}
|
|
2849
|
+
return {
|
|
2850
|
+
appType: value.appType,
|
|
2851
|
+
activeAppReleaseId:
|
|
2852
|
+
value.activeAppReleaseId || release.id || null,
|
|
2853
|
+
...(value.activeRuntimeReleaseId
|
|
2854
|
+
? { activeRuntimeReleaseId: value.activeRuntimeReleaseId }
|
|
2855
|
+
: {}),
|
|
2856
|
+
...(value.activePageReleaseId
|
|
2857
|
+
? { activePageReleaseId: value.activePageReleaseId }
|
|
2858
|
+
: {}),
|
|
2859
|
+
...(value.activeBackendReleaseId
|
|
2860
|
+
? { activeBackendReleaseId: value.activeBackendReleaseId }
|
|
2861
|
+
: {}),
|
|
2862
|
+
release: compactRelease,
|
|
2863
|
+
resourceCount: resources.length,
|
|
2864
|
+
resourcesByKind,
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2599
2868
|
async function runAppReleaseCommand(
|
|
2600
2869
|
config,
|
|
2601
2870
|
target,
|
|
@@ -2613,6 +2882,7 @@ async function runAppReleaseCommand(
|
|
|
2613
2882
|
target.profileName,
|
|
2614
2883
|
appReleaseApiPath(target, '/head')
|
|
2615
2884
|
);
|
|
2885
|
+
if (!flags.full) data = summarizeAppReleaseHead(data);
|
|
2616
2886
|
} else if (subcommand === 'app-list') {
|
|
2617
2887
|
data = await requestWithAuth(
|
|
2618
2888
|
config,
|
|
@@ -2835,7 +3105,17 @@ async function runAppReleaseCommand(
|
|
|
2835
3105
|
let stateReconcileWarning = null;
|
|
2836
3106
|
try {
|
|
2837
3107
|
const appHead = await loadRuntimeHeadApp(config, target);
|
|
2838
|
-
|
|
3108
|
+
const stagedRuntime = stagedResources?.find(
|
|
3109
|
+
resource => resource.kind === 'RuntimeRelease'
|
|
3110
|
+
);
|
|
3111
|
+
saveRuntimeHeadState(target, appHead, {
|
|
3112
|
+
id:
|
|
3113
|
+
stagedRuntime?.identity?.releaseId ||
|
|
3114
|
+
stagedRuntime?.releaseId ||
|
|
3115
|
+
null,
|
|
3116
|
+
buildId: stagedRuntime?.metadata?.buildId || null,
|
|
3117
|
+
assetBaseUrl: stagedRuntime?.metadata?.assetBaseUrl || null,
|
|
3118
|
+
});
|
|
2839
3119
|
} catch (error) {
|
|
2840
3120
|
stateReconcileWarning = {
|
|
2841
3121
|
component: 'RuntimeStateReadback',
|
|
@@ -9687,11 +9967,11 @@ async function runtime(args) {
|
|
|
9687
9967
|
const { flags, positional } = parseArgs(rest);
|
|
9688
9968
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
9689
9969
|
print([
|
|
9690
|
-
'用法: openxiangda runtime deploy|releases|activate [--profile name] [--json]',
|
|
9970
|
+
'用法: openxiangda runtime deploy|releases [--full]|activate [--profile name] [--json]',
|
|
9691
9971
|
'常用流程:',
|
|
9692
9972
|
' npm run build',
|
|
9693
9973
|
' openxiangda runtime deploy --no-activate --change <change> --profile <name> --json',
|
|
9694
|
-
' openxiangda runtime releases --profile <name>',
|
|
9974
|
+
' openxiangda runtime releases --profile <name> [--full]',
|
|
9695
9975
|
'说明:',
|
|
9696
9976
|
' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
|
|
9697
9977
|
' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到平台内置对象存储直传。',
|
|
@@ -9718,8 +9998,9 @@ async function runtime(args) {
|
|
|
9718
9998
|
target.profileName,
|
|
9719
9999
|
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`
|
|
9720
10000
|
);
|
|
9721
|
-
|
|
9722
|
-
|
|
10001
|
+
const output = flags.full ? data : summarizeRuntimeReleaseList(data);
|
|
10002
|
+
if (flags.json) return writeJson(output);
|
|
10003
|
+
print(JSON.stringify(output, null, 2));
|
|
9723
10004
|
return;
|
|
9724
10005
|
}
|
|
9725
10006
|
|
|
@@ -10032,6 +10313,7 @@ async function runtime(args) {
|
|
|
10032
10313
|
const runtimeContentHash = String(data?.contentHash || '')
|
|
10033
10314
|
.trim()
|
|
10034
10315
|
.toLowerCase();
|
|
10316
|
+
const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
|
|
10035
10317
|
const stagedResource = flags['no-activate']
|
|
10036
10318
|
? stripUndefinedValues({
|
|
10037
10319
|
kind: 'RuntimeRelease',
|
|
@@ -10045,6 +10327,7 @@ async function runtime(args) {
|
|
|
10045
10327
|
metadata: {
|
|
10046
10328
|
buildId: data?.buildId || buildId,
|
|
10047
10329
|
releaseStatus: data?.status || 'staged',
|
|
10330
|
+
assetBaseUrl: releaseAssetBaseUrl,
|
|
10048
10331
|
},
|
|
10049
10332
|
})
|
|
10050
10333
|
: null;
|
|
@@ -10069,7 +10352,6 @@ async function runtime(args) {
|
|
|
10069
10352
|
target
|
|
10070
10353
|
)
|
|
10071
10354
|
: null;
|
|
10072
|
-
const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
|
|
10073
10355
|
const result = {
|
|
10074
10356
|
appType: target.appType,
|
|
10075
10357
|
buildId,
|
|
@@ -10115,7 +10397,7 @@ async function runtime(args) {
|
|
|
10115
10397
|
}
|
|
10116
10398
|
}
|
|
10117
10399
|
|
|
10118
|
-
fail('用法: openxiangda runtime deploy|releases|activate [--profile name]');
|
|
10400
|
+
fail('用法: openxiangda runtime deploy|releases [--full]|activate [--profile name]');
|
|
10119
10401
|
}
|
|
10120
10402
|
|
|
10121
10403
|
async function loadRuntimeHeadApp(config, target) {
|
|
@@ -10302,9 +10584,11 @@ async function uploadRuntimeDistFilesStaged(options) {
|
|
|
10302
10584
|
async file => {
|
|
10303
10585
|
const uploaded = await uploadRuntimeDistFile(options, file);
|
|
10304
10586
|
completed += 1;
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10587
|
+
if (shouldReportRuntimeUploadProgress(completed, files.length)) {
|
|
10588
|
+
printRuntimeProgress(
|
|
10589
|
+
`runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
|
|
10590
|
+
);
|
|
10591
|
+
}
|
|
10308
10592
|
return uploaded;
|
|
10309
10593
|
}
|
|
10310
10594
|
);
|
|
@@ -10359,9 +10643,11 @@ async function uploadRuntimeDistFilesOssDirect(options) {
|
|
|
10359
10643
|
);
|
|
10360
10644
|
const uploaded = await uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo);
|
|
10361
10645
|
completed += 1;
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10646
|
+
if (shouldReportRuntimeUploadProgress(completed, files.length)) {
|
|
10647
|
+
printRuntimeProgress(
|
|
10648
|
+
`runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
|
|
10649
|
+
);
|
|
10650
|
+
}
|
|
10365
10651
|
return { ...uploaded, storageProvider };
|
|
10366
10652
|
}
|
|
10367
10653
|
);
|
|
@@ -10623,21 +10909,71 @@ function saveRuntimeReleaseState(target, release) {
|
|
|
10623
10909
|
saveProjectState(target.state);
|
|
10624
10910
|
}
|
|
10625
10911
|
|
|
10626
|
-
function saveRuntimeHeadState(target, app) {
|
|
10912
|
+
function saveRuntimeHeadState(target, app, fallbackRelease = {}) {
|
|
10627
10913
|
const activeReleaseId =
|
|
10628
|
-
app?.activeRuntimeReleaseId ||
|
|
10914
|
+
app?.activeRuntimeReleaseId ||
|
|
10915
|
+
app?.activeRuntime?.releaseId ||
|
|
10916
|
+
fallbackRelease?.id ||
|
|
10917
|
+
fallbackRelease?.releaseId ||
|
|
10918
|
+
null;
|
|
10629
10919
|
const activeBuildId =
|
|
10630
|
-
app?.activeRuntimeBuildId ||
|
|
10920
|
+
app?.activeRuntimeBuildId ||
|
|
10921
|
+
app?.activeRuntime?.buildId ||
|
|
10922
|
+
fallbackRelease?.buildId ||
|
|
10923
|
+
null;
|
|
10631
10924
|
if (!activeReleaseId && !activeBuildId) return false;
|
|
10632
10925
|
saveRuntimeReleaseState(target, {
|
|
10633
10926
|
id: activeReleaseId,
|
|
10634
10927
|
buildId: activeBuildId,
|
|
10635
10928
|
assetBaseUrl:
|
|
10636
|
-
app?.runtimeAssetBaseUrl ||
|
|
10929
|
+
app?.runtimeAssetBaseUrl ||
|
|
10930
|
+
app?.activeRuntime?.assetBaseUrl ||
|
|
10931
|
+
fallbackRelease?.assetBaseUrl ||
|
|
10932
|
+
null,
|
|
10637
10933
|
});
|
|
10638
10934
|
return true;
|
|
10639
10935
|
}
|
|
10640
10936
|
|
|
10937
|
+
function summarizeRuntimeReleaseList(data) {
|
|
10938
|
+
const items = Array.isArray(data)
|
|
10939
|
+
? data
|
|
10940
|
+
: Array.isArray(data?.items)
|
|
10941
|
+
? data.items
|
|
10942
|
+
: [];
|
|
10943
|
+
const summarized = items.map(item =>
|
|
10944
|
+
stripUndefinedValues({
|
|
10945
|
+
id: item?.id,
|
|
10946
|
+
version: item?.version,
|
|
10947
|
+
buildId: item?.buildId,
|
|
10948
|
+
status: item?.status,
|
|
10949
|
+
storageProvider: item?.storageProvider,
|
|
10950
|
+
fileCount: item?.fileCount,
|
|
10951
|
+
sizeBytes: item?.sizeBytes,
|
|
10952
|
+
contentHash: item?.contentHash,
|
|
10953
|
+
sourceRevision: item?.sourceRevision,
|
|
10954
|
+
parentReleaseId: item?.parentReleaseId,
|
|
10955
|
+
parentSourceRevision: item?.parentSourceRevision,
|
|
10956
|
+
lineageBypassReason: item?.lineageBypassReason,
|
|
10957
|
+
lineageBootstrap: item?.lineageBootstrap,
|
|
10958
|
+
releaseNotes: item?.releaseNotes,
|
|
10959
|
+
createdAt: item?.createdAt,
|
|
10960
|
+
activatedAt: item?.activatedAt,
|
|
10961
|
+
})
|
|
10962
|
+
);
|
|
10963
|
+
return Array.isArray(data)
|
|
10964
|
+
? summarized
|
|
10965
|
+
: {
|
|
10966
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
10967
|
+
items: summarized,
|
|
10968
|
+
};
|
|
10969
|
+
}
|
|
10970
|
+
|
|
10971
|
+
function shouldReportRuntimeUploadProgress(completed, total) {
|
|
10972
|
+
if (total <= 10) return true;
|
|
10973
|
+
const interval = Math.max(1, Math.ceil(total / 10));
|
|
10974
|
+
return completed === 1 || completed === total || completed % interval === 0;
|
|
10975
|
+
}
|
|
10976
|
+
|
|
10641
10977
|
function formatBytes(value) {
|
|
10642
10978
|
const bytes = Number(value || 0);
|
|
10643
10979
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -14084,7 +14420,7 @@ async function tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
|
|
|
14084
14420
|
|
|
14085
14421
|
let usedLegacyBuilder = false;
|
|
14086
14422
|
for (const sourceInfo of sourceInfos) {
|
|
14087
|
-
const result =
|
|
14423
|
+
const result = runLegacyWorkspaceJsCodeBuild(
|
|
14088
14424
|
workspaceRoot,
|
|
14089
14425
|
sourceInfo.scriptCode,
|
|
14090
14426
|
sourceInfo.sourceKind
|
|
@@ -20971,6 +21307,27 @@ function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind)
|
|
|
20971
21307
|
);
|
|
20972
21308
|
}
|
|
20973
21309
|
|
|
21310
|
+
function runLegacyWorkspaceJsCodeBuild(
|
|
21311
|
+
workspaceRoot,
|
|
21312
|
+
resolvedScriptCode,
|
|
21313
|
+
sourceKind
|
|
21314
|
+
) {
|
|
21315
|
+
return spawnSync(
|
|
21316
|
+
'pnpm',
|
|
21317
|
+
[
|
|
21318
|
+
'build-js-code',
|
|
21319
|
+
'--script',
|
|
21320
|
+
resolvedScriptCode,
|
|
21321
|
+
'--source',
|
|
21322
|
+
sourceKind,
|
|
21323
|
+
],
|
|
21324
|
+
{
|
|
21325
|
+
cwd: workspaceRoot,
|
|
21326
|
+
encoding: 'utf8',
|
|
21327
|
+
}
|
|
21328
|
+
);
|
|
21329
|
+
}
|
|
21330
|
+
|
|
20974
21331
|
function isUnsupportedFunctionsBuildScript(result) {
|
|
20975
21332
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
20976
21333
|
return (
|
package/lib/release-mainline.js
CHANGED
|
@@ -264,6 +264,12 @@ function resolveRemoteMainline({ cwd, remoteName }) {
|
|
|
264
264
|
`远端 ${remoteName} 同时存在 main 与 master 且无法读取 remote HEAD;请先修复远端默认分支 HEAD`
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
|
+
if (isGitAuthenticationError(remoteHead.stderr)) {
|
|
268
|
+
throw releaseMainlineError(
|
|
269
|
+
'RELEASE_GIT_AUTH_REQUIRED',
|
|
270
|
+
`无法认证权威远端 ${remoteName}。请先解锁系统钥匙串或配置可用的 Git credential helper,并确认 \`git ls-remote --symref ${remoteName} HEAD\` 可成功执行;发布会在获取平台租约前停止,不会留下远端发布状态`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
267
273
|
const detail = remoteHead.stderr
|
|
268
274
|
? `: ${sanitizeGitError(remoteHead.stderr)}`
|
|
269
275
|
: '';
|
|
@@ -273,6 +279,12 @@ function resolveRemoteMainline({ cwd, remoteName }) {
|
|
|
273
279
|
);
|
|
274
280
|
}
|
|
275
281
|
|
|
282
|
+
function isGitAuthenticationError(value) {
|
|
283
|
+
return /(?:authentication failed|could not read (?:username|password)|terminal prompts? disabled|credential|keychain|access denied|http basic: access denied|response:\s*(?:401|403)|returned error:\s*(?:401|403))/i.test(
|
|
284
|
+
String(value || '')
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
276
288
|
function readRemoteBranchTip(cwd, remoteName, branchName, options = {}) {
|
|
277
289
|
const result = readGit(
|
|
278
290
|
cwd,
|
|
@@ -577,6 +589,7 @@ module.exports = {
|
|
|
577
589
|
assertReleaseSourceIntegrated,
|
|
578
590
|
assertReleaseSourceRevisionStable,
|
|
579
591
|
inspectReleaseSourceIntegration,
|
|
592
|
+
isGitAuthenticationError,
|
|
580
593
|
prepareReleaseSourceRevision,
|
|
581
594
|
resolveAuthoritativeMainline,
|
|
582
595
|
};
|
package/lib/sdd.js
CHANGED
|
@@ -115,9 +115,12 @@ function readJsonFile(file, fallback = null) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function writeJsonFile(file, value) {
|
|
118
|
+
function writeJsonFile(file, value, options = {}) {
|
|
119
119
|
ensureDir(path.dirname(file));
|
|
120
|
-
fs.writeFileSync(
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
file,
|
|
122
|
+
`${JSON.stringify(value, null, options.compact ? 0 : 2)}\n`
|
|
123
|
+
);
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function extractSddBlock(configText) {
|
|
@@ -578,6 +581,15 @@ function formatAffectedList(items) {
|
|
|
578
581
|
}
|
|
579
582
|
|
|
580
583
|
function createProposalContent(meta) {
|
|
584
|
+
if (meta.changeMode === 'quick') {
|
|
585
|
+
return [
|
|
586
|
+
`# Quick Change: ${meta.title}`,
|
|
587
|
+
'',
|
|
588
|
+
`User intent and approval are recorded in \`${CHANGE_META_FILE}\`.`,
|
|
589
|
+
`Exact resource/file scope is recorded in \`${COVERAGE_FILE}\`; no additional proposal prose is required.`,
|
|
590
|
+
'',
|
|
591
|
+
].join('\n');
|
|
592
|
+
}
|
|
581
593
|
return `# Proposal: ${meta.title}
|
|
582
594
|
|
|
583
595
|
## Intent
|
|
@@ -606,6 +618,14 @@ Deliver "${meta.title}" as one focused OpenXiangda application change.
|
|
|
606
618
|
}
|
|
607
619
|
|
|
608
620
|
function createDesignContent(meta) {
|
|
621
|
+
if (meta.changeMode === 'quick') {
|
|
622
|
+
return [
|
|
623
|
+
'# Design',
|
|
624
|
+
'',
|
|
625
|
+
'L1 exact-scope change; implementation stays inside coverage.json and uses the generated atomic release plan.',
|
|
626
|
+
'',
|
|
627
|
+
].join('\n');
|
|
628
|
+
}
|
|
609
629
|
return `# Design: ${meta.title}
|
|
610
630
|
|
|
611
631
|
## OpenXiangda Resource Impact
|
|
@@ -640,22 +660,19 @@ function createTasksContent(meta = {}) {
|
|
|
640
660
|
'',
|
|
641
661
|
'## Implementation',
|
|
642
662
|
'',
|
|
643
|
-
'- [
|
|
644
|
-
'- [ ] Implement the focused change',
|
|
663
|
+
'- [ ] Implement the exact covered change',
|
|
645
664
|
'',
|
|
646
665
|
'## Prepublish',
|
|
647
666
|
'',
|
|
648
|
-
'- [ ]
|
|
649
|
-
'- [ ] Confirm the exact release plan',
|
|
667
|
+
'- [ ] Run the focused check and confirm the generated release plan',
|
|
650
668
|
'',
|
|
651
669
|
'## Postpublish',
|
|
652
670
|
'',
|
|
653
|
-
'- [ ] Record publish
|
|
654
|
-
'- [ ] Merge/fast-forward the frozen release SHA and push the authoritative default branch',
|
|
671
|
+
'- [ ] Record atomic publish and mainline integration status',
|
|
655
672
|
'',
|
|
656
673
|
'## Archive',
|
|
657
674
|
'',
|
|
658
|
-
'- [ ]
|
|
675
|
+
'- [ ] Archive the released change',
|
|
659
676
|
'',
|
|
660
677
|
].join('\n');
|
|
661
678
|
}
|
|
@@ -686,7 +703,21 @@ function createTasksContent(meta = {}) {
|
|
|
686
703
|
].join('\n');
|
|
687
704
|
}
|
|
688
705
|
|
|
689
|
-
function createEvidenceContent() {
|
|
706
|
+
function createEvidenceContent(meta = {}) {
|
|
707
|
+
if (meta.changeMode === 'quick') {
|
|
708
|
+
return [
|
|
709
|
+
'# Evidence',
|
|
710
|
+
'',
|
|
711
|
+
'## Verification',
|
|
712
|
+
'',
|
|
713
|
+
'- [ ] Record the focused check result',
|
|
714
|
+
'',
|
|
715
|
+
'## Release',
|
|
716
|
+
'',
|
|
717
|
+
'- [ ] Record the atomic publish result',
|
|
718
|
+
'',
|
|
719
|
+
].join('\n');
|
|
720
|
+
}
|
|
690
721
|
return [
|
|
691
722
|
'# Evidence',
|
|
692
723
|
'',
|
|
@@ -708,6 +739,22 @@ function createEvidenceContent() {
|
|
|
708
739
|
}
|
|
709
740
|
|
|
710
741
|
function createDeltaSpecContent(meta, domain) {
|
|
742
|
+
if (meta.changeMode === 'quick') {
|
|
743
|
+
return [
|
|
744
|
+
`# ${domain} Delta Spec`,
|
|
745
|
+
'',
|
|
746
|
+
'## ADDED Requirements',
|
|
747
|
+
'',
|
|
748
|
+
`### Requirement: ${meta.title}`,
|
|
749
|
+
'The application SHALL preserve existing behavior except for the exact approved L1 scope.',
|
|
750
|
+
'',
|
|
751
|
+
'#### Scenario: Focused change',
|
|
752
|
+
'- **Given** the existing application behavior',
|
|
753
|
+
`- **When** ${meta.title} is applied`,
|
|
754
|
+
'- **Then** only the covered target changes and unrelated behavior remains unchanged',
|
|
755
|
+
'',
|
|
756
|
+
].join('\n');
|
|
757
|
+
}
|
|
711
758
|
return `# ${domain} Delta Spec
|
|
712
759
|
|
|
713
760
|
## ADDED Requirements
|
|
@@ -807,13 +854,16 @@ function proposeSddChange(options = {}) {
|
|
|
807
854
|
const meta = createChangeMetadata(changeId, options);
|
|
808
855
|
const coverage = createCoverageManifest(meta, domain);
|
|
809
856
|
const release = createReleaseManifest(meta);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
857
|
+
const jsonWriteOptions = {
|
|
858
|
+
compact: meta.changeMode === 'quick',
|
|
859
|
+
};
|
|
860
|
+
writeJsonFile(path.join(changeDir, CHANGE_META_FILE), meta, jsonWriteOptions);
|
|
861
|
+
writeJsonFile(path.join(changeDir, COVERAGE_FILE), coverage, jsonWriteOptions);
|
|
862
|
+
writeJsonFile(path.join(changeDir, RELEASE_FILE), release, jsonWriteOptions);
|
|
813
863
|
fs.writeFileSync(path.join(changeDir, 'proposal.md'), createProposalContent(meta));
|
|
814
864
|
fs.writeFileSync(path.join(changeDir, 'design.md'), createDesignContent(meta));
|
|
815
865
|
fs.writeFileSync(path.join(changeDir, 'tasks.md'), createTasksContent(meta));
|
|
816
|
-
fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent());
|
|
866
|
+
fs.writeFileSync(path.join(changeDir, 'evidence.md'), createEvidenceContent(meta));
|
|
817
867
|
fs.writeFileSync(
|
|
818
868
|
path.join(changeDir, 'specs', domain, 'spec.md'),
|
|
819
869
|
createDeltaSpecContent(meta, domain)
|
|
@@ -1024,7 +1074,9 @@ function approveSddChange(options = {}) {
|
|
|
1024
1074
|
...(options.approvalBasis ? { approvalBasis: options.approvalBasis } : {}),
|
|
1025
1075
|
updatedAt: nowIso(),
|
|
1026
1076
|
};
|
|
1027
|
-
writeJsonFile(loaded.metaFile, meta
|
|
1077
|
+
writeJsonFile(loaded.metaFile, meta, {
|
|
1078
|
+
compact: meta.changeMode === 'quick',
|
|
1079
|
+
});
|
|
1028
1080
|
return {
|
|
1029
1081
|
schemaVersion: meta.schemaVersion,
|
|
1030
1082
|
change: meta,
|
|
@@ -2052,6 +2104,7 @@ function validateReleaseManifest(
|
|
|
2052
2104
|
}
|
|
2053
2105
|
if (
|
|
2054
2106
|
!legacyMode &&
|
|
2107
|
+
!options.actualExecution &&
|
|
2055
2108
|
Array.isArray(release.commands) &&
|
|
2056
2109
|
release.commands.length > 0
|
|
2057
2110
|
) {
|
|
@@ -60,6 +60,8 @@ openxiangda sdd quick <change> --kind copy --pages <page> --files <file> --summa
|
|
|
60
60
|
|
|
61
61
|
Function quick changes require the complete `--risk-json` assessment; Automation, Workflow, schema, permissions, auth/public access, migrations, destructive and cross-resource changes cannot use quick mode. Before release run `--stage prepublish`; use `postpublish` only after promotion and `archive` only when all evidence exists.
|
|
62
62
|
|
|
63
|
+
Quick changes use compact generated JSON/prose. Do not expand them into proposal/design essays; implement the exact covered code, run the focused check, and let the mainline bundle own final atomic promotion.
|
|
64
|
+
|
|
63
65
|
Keep development lightweight: structured approval plus exact resource/file scope are authoritative. By default, unfinished tasks/evidence/spec prose produces warnings at every stage and does not block release; set `strictDocumentation: true` only for a workspace that intentionally wants prose as a gate. Actual publish argv, mainline identity, CAS, lease, destructive scope, and atomic activation remain hard gates.
|
|
64
66
|
|
|
65
67
|
The CLI regenerates one canonical command set from structured coverage instead of asking agents to maintain two command copies. A single Function uses `resource publish function --only <code>`; qualified selectors are used only when Function and Automation are mixed. Generated React SPA plans use exact Form bundles, one Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`. Actual argv is validated again at write time.
|
|
@@ -89,6 +91,8 @@ openxiangda release publish --change <release-change> --profile <name>
|
|
|
89
91
|
|
|
90
92
|
`release publish` is the default promotion entrypoint. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. `release begin` and child commands remain recovery/diagnostic primitives.
|
|
91
93
|
|
|
94
|
+
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes are logical coverage targets and activate through one Runtime child; they do not require PageRelease. `release app-head` and `runtime releases` are compact by default; use `--full` only when the complete manifest is required.
|
|
95
|
+
|
|
92
96
|
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences are aliases of the same repository; genuinely different remotes still fail closed.
|
|
93
97
|
|
|
94
98
|
Because promotion begins from the already-pushed authoritative mainline, `release integration-status` should pass immediately after activation. Run it, then `release end`; no post-release branch merge is required.
|
|
@@ -94,8 +94,12 @@ openxiangda release publish --change <release-change> --profile <name>
|
|
|
94
94
|
|
|
95
95
|
`release publish` is the normal whole-app entrypoint: it verifies SDD without mutating reviewed files, waits for the promotion lease, freezes one App capture, stages the exact Form/Backend/Runtime children, resumes from a private execution journal, finalizes once, and releases the lease. Individual release commands are recovery/diagnostic primitives.
|
|
96
96
|
|
|
97
|
+
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes remain logical coverage targets and activate through the single Runtime child; they do not require PageRelease. If local lease state disappears, `release end --change <id>` reconciles a self-owned remote lease from the private execution journal and never reports inactive while a remote lease is active.
|
|
98
|
+
|
|
97
99
|
`runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. It reads a narrow Runtime head instead of the full app snapshot. Before acquiring a lease, the CLI confirms `package.json#scripts.build` and existing dependencies; it uses `npm run build` to execute the declared script so pnpm worktree symlinks do not trigger a reinstall. Immutable Git-base artifact hashes are cached under the Git common directory. `release begin` requires local main/master and the live remote default tip to be identical, so `integration-status` is already satisfied after activation and `release end` does not wait for a later merge.
|
|
98
100
|
|
|
101
|
+
`release app-head` and `runtime releases` return compact summaries by default. Use `--full` only when the complete manifest is required.
|
|
102
|
+
|
|
99
103
|
`resource plan`, `resource publish`, and `runtime deploy` report coarse phases on stderr and emit a heartbeat after 15 seconds without completion. JSON stdout stays machine-readable and includes `timings`; use it to identify whether time is spent in plan, preflight, build, upload, write, or activate instead of rerunning an opaque command.
|
|
100
104
|
|
|
101
105
|
Use `--upload-mode legacy-json` only for an old platform that lacks staged uploads. Keep timeout/progress on stderr so `--json` stdout remains machine-readable.
|