openxiangda 1.0.173 → 1.0.174
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 +296 -30
- 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` returns a compact summary 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,并在同一事务中原子激活。',
|
|
@@ -1599,34 +1805,22 @@ async function release(args) {
|
|
|
1599
1805
|
return;
|
|
1600
1806
|
}
|
|
1601
1807
|
if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
|
|
1602
|
-
assertOrClaimWorktreeOwner({
|
|
1603
|
-
cwd: process.cwd(),
|
|
1604
|
-
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1605
|
-
});
|
|
1606
1808
|
const leaseId = leaseIdFromArgs || stored?.leaseId;
|
|
1607
1809
|
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() });
|
|
1810
|
+
const result = await endReleaseWithoutLocalLease(
|
|
1811
|
+
config,
|
|
1812
|
+
target,
|
|
1813
|
+
flags,
|
|
1814
|
+
baseline
|
|
1815
|
+
);
|
|
1626
1816
|
if (flags.json) return writeJson(result);
|
|
1627
1817
|
print(`当前工作区没有活动发布租约: ${target.appType}`);
|
|
1628
1818
|
return;
|
|
1629
1819
|
}
|
|
1820
|
+
assertOrClaimWorktreeOwner({
|
|
1821
|
+
cwd: process.cwd(),
|
|
1822
|
+
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1823
|
+
});
|
|
1630
1824
|
const integration = assertReleaseSourceIntegrated(
|
|
1631
1825
|
baseline?.releaseSourceRevision,
|
|
1632
1826
|
{ cwd: process.cwd() }
|
|
@@ -2427,8 +2621,15 @@ function changedAppReleaseResources(detailResources, captureResources) {
|
|
|
2427
2621
|
}
|
|
2428
2622
|
|
|
2429
2623
|
function assertAppReleaseActualGate(target, flags, scope, command) {
|
|
2624
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(
|
|
2625
|
+
readWorkspaceConfigText()
|
|
2626
|
+
);
|
|
2627
|
+
const normalized = normalizeReleaseTargets(
|
|
2628
|
+
scope.targets || {},
|
|
2629
|
+
runtimeMode
|
|
2630
|
+
);
|
|
2430
2631
|
assertSddReleaseGate(
|
|
2431
|
-
buildExplicitSddReleasePlan(
|
|
2632
|
+
buildExplicitSddReleasePlan(normalized.activationTargets, {
|
|
2432
2633
|
files: scope.changedFiles || [],
|
|
2433
2634
|
commands: [command],
|
|
2434
2635
|
changeId: scope.changeId,
|
|
@@ -2596,6 +2797,70 @@ async function activateAppReleaseDetail(
|
|
|
2596
2797
|
);
|
|
2597
2798
|
}
|
|
2598
2799
|
|
|
2800
|
+
function summarizeAppReleaseHead(data) {
|
|
2801
|
+
const value =
|
|
2802
|
+
data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
2803
|
+
const release =
|
|
2804
|
+
value.release &&
|
|
2805
|
+
typeof value.release === 'object' &&
|
|
2806
|
+
!Array.isArray(value.release)
|
|
2807
|
+
? value.release
|
|
2808
|
+
: {};
|
|
2809
|
+
const manifest =
|
|
2810
|
+
release.manifestJson &&
|
|
2811
|
+
typeof release.manifestJson === 'object'
|
|
2812
|
+
? release.manifestJson
|
|
2813
|
+
: value.manifestJson &&
|
|
2814
|
+
typeof value.manifestJson === 'object'
|
|
2815
|
+
? value.manifestJson
|
|
2816
|
+
: {};
|
|
2817
|
+
const resources = Array.isArray(manifest.resources)
|
|
2818
|
+
? manifest.resources
|
|
2819
|
+
: Array.isArray(value.resources)
|
|
2820
|
+
? value.resources
|
|
2821
|
+
: [];
|
|
2822
|
+
const resourcesByKind = Object.fromEntries(
|
|
2823
|
+
Array.from(
|
|
2824
|
+
resources.reduce((counts, resource) => {
|
|
2825
|
+
const kind = String(resource?.kind || 'Unknown');
|
|
2826
|
+
counts.set(kind, (counts.get(kind) || 0) + 1);
|
|
2827
|
+
return counts;
|
|
2828
|
+
}, new Map())
|
|
2829
|
+
).sort(([left], [right]) => left.localeCompare(right))
|
|
2830
|
+
);
|
|
2831
|
+
const compactRelease = {};
|
|
2832
|
+
for (const key of [
|
|
2833
|
+
'id',
|
|
2834
|
+
'status',
|
|
2835
|
+
'parentReleaseId',
|
|
2836
|
+
'manifestHash',
|
|
2837
|
+
'verificationHash',
|
|
2838
|
+
'protocolVersion',
|
|
2839
|
+
'createdAt',
|
|
2840
|
+
'updatedAt',
|
|
2841
|
+
'activatedAt',
|
|
2842
|
+
]) {
|
|
2843
|
+
if (release[key] !== undefined) compactRelease[key] = release[key];
|
|
2844
|
+
}
|
|
2845
|
+
return {
|
|
2846
|
+
appType: value.appType,
|
|
2847
|
+
activeAppReleaseId:
|
|
2848
|
+
value.activeAppReleaseId || release.id || null,
|
|
2849
|
+
...(value.activeRuntimeReleaseId
|
|
2850
|
+
? { activeRuntimeReleaseId: value.activeRuntimeReleaseId }
|
|
2851
|
+
: {}),
|
|
2852
|
+
...(value.activePageReleaseId
|
|
2853
|
+
? { activePageReleaseId: value.activePageReleaseId }
|
|
2854
|
+
: {}),
|
|
2855
|
+
...(value.activeBackendReleaseId
|
|
2856
|
+
? { activeBackendReleaseId: value.activeBackendReleaseId }
|
|
2857
|
+
: {}),
|
|
2858
|
+
release: compactRelease,
|
|
2859
|
+
resourceCount: resources.length,
|
|
2860
|
+
resourcesByKind,
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2599
2864
|
async function runAppReleaseCommand(
|
|
2600
2865
|
config,
|
|
2601
2866
|
target,
|
|
@@ -2613,6 +2878,7 @@ async function runAppReleaseCommand(
|
|
|
2613
2878
|
target.profileName,
|
|
2614
2879
|
appReleaseApiPath(target, '/head')
|
|
2615
2880
|
);
|
|
2881
|
+
if (!flags.full) data = summarizeAppReleaseHead(data);
|
|
2616
2882
|
} else if (subcommand === 'app-list') {
|
|
2617
2883
|
data = await requestWithAuth(
|
|
2618
2884
|
config,
|
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` is 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` returns a compact head/resource-count summary 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.
|