openxiangda 1.0.162 → 1.0.164

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.
Files changed (33) hide show
  1. package/README.md +6 -2
  2. package/lib/change-baseline.js +88 -2
  3. package/lib/cli.js +565 -73
  4. package/lib/js-code-build.js +120 -8
  5. package/lib/publish-lease.js +13 -1
  6. package/lib/release-mainline.js +545 -0
  7. package/lib/runtime-lineage.js +19 -12
  8. package/lib/sdd.js +137 -0
  9. package/lib/workspace-bootstrap.js +36 -7
  10. package/lib/workspace-init.js +12 -3
  11. package/lib/worktree-owner.js +13 -0
  12. package/openxiangda-skills/SKILL.md +9 -3
  13. package/openxiangda-skills/references/automation-v3.md +1 -1
  14. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
  15. package/openxiangda-skills/references/workflow-v3.md +1 -1
  16. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -2
  17. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
  18. package/package.json +3 -1
  19. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +2 -1
  20. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -2
  21. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +2 -1
  22. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
  23. package/templates/openxiangda-react-spa/AGENTS.md +8 -6
  24. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +172 -20
  25. package/templates/openxiangda-react-spa/scripts/deploy.mjs +19 -39
  26. package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +4 -5
  27. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +3 -1
  28. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
  29. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +3 -1
  30. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
  31. package/templates/sy-lowcode-app-workspace/AGENTS.md +4 -4
  32. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +172 -20
  33. package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +4 -5
@@ -0,0 +1,545 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const { readGitSourceBase } = require('./change-baseline');
6
+ const { listRuntimeBuildInputChanges } = require('./runtime-lineage');
7
+
8
+ const GIT_OUTPUT_LIMIT = 16 * 1024 * 1024;
9
+
10
+ function prepareReleaseSourceRevision(options = {}) {
11
+ assertNoMainlineOverride(options);
12
+ const cwd = path.resolve(options.cwd || process.cwd());
13
+ const sourceRevision = readGitSourceBase(cwd, 'HEAD');
14
+ const branch = readCurrentBranch(cwd);
15
+ if (!branch) {
16
+ throw releaseMainlineError(
17
+ 'RELEASE_SOURCE_BRANCH_REQUIRED',
18
+ '正式发布必须从具名 Git 分支冻结源码;detached HEAD 只能用于只读检查或本地构建'
19
+ );
20
+ }
21
+
22
+ const dirtyPaths = listRuntimeBuildInputChanges(cwd);
23
+ if (dirtyPaths.length > 0) {
24
+ throw releaseMainlineError(
25
+ 'RELEASE_SOURCE_DIRTY',
26
+ `正式发布前必须提交全部源码和构建配置;未提交路径: ${summarizePaths(dirtyPaths)}`,
27
+ { dirtyPaths }
28
+ );
29
+ }
30
+
31
+ const mainline = resolveAuthoritativeMainline({ cwd });
32
+ ensureCommitAvailable(
33
+ cwd,
34
+ mainline.tipCommit,
35
+ mainline.remoteName,
36
+ mainline.branchName
37
+ );
38
+ if (
39
+ !isGitAncestor(cwd, mainline.tipCommit, sourceRevision.baseCommit, {
40
+ remoteName: mainline.remoteName,
41
+ branchName: mainline.branchName,
42
+ })
43
+ ) {
44
+ throw releaseMainlineError(
45
+ 'RELEASE_SOURCE_BEHIND_MAIN',
46
+ `发布提交 ${shortCommit(sourceRevision.baseCommit)} 未包含权威主分支 ${formatMainline(mainline)}@${shortCommit(mainline.tipCommit)};请先合并主分支、重新测试并重新开始 release`,
47
+ { sourceRevision, mainline }
48
+ );
49
+ }
50
+
51
+ return {
52
+ ...sourceRevision,
53
+ branch,
54
+ worktreeRoot: readGit(cwd, ['rev-parse', '--show-toplevel']).stdout,
55
+ remoteName: mainline.remoteName,
56
+ remoteUrlHash: mainline.remoteUrlHash,
57
+ mainBranch: mainline.branchName,
58
+ mainTipCommit: mainline.tipCommit,
59
+ mainlineSource: mainline.source,
60
+ capturedAt: new Date().toISOString(),
61
+ };
62
+ }
63
+
64
+ function assertReleaseSourceRevisionStable(sourceRevision, options = {}) {
65
+ const cwd = path.resolve(options.cwd || process.cwd());
66
+ const expected = normalizeSourceRevision(sourceRevision);
67
+ const current = readGitSourceBase(cwd, 'HEAD');
68
+ const dirtyPaths = listRuntimeBuildInputChanges(cwd);
69
+ if (
70
+ expected.repo !== current.repo ||
71
+ expected.baseCommit !== current.baseCommit ||
72
+ expected.treeHash !== current.treeHash ||
73
+ dirtyPaths.length > 0
74
+ ) {
75
+ throw releaseMainlineError(
76
+ 'RELEASE_PUBLISH_REVISION_CHANGED',
77
+ [
78
+ `release 已冻结 ${shortCommit(expected.baseCommit)},当前源码为 ${shortCommit(current.baseCommit)}`,
79
+ dirtyPaths.length > 0
80
+ ? `且存在未提交路径: ${summarizePaths(dirtyPaths)}`
81
+ : '',
82
+ '发布期间禁止切换提交或修改构建输入;请完成当前提交的主线回合并结束 release 后重新计划',
83
+ ]
84
+ .filter(Boolean)
85
+ .join(' '),
86
+ { expected, current, dirtyPaths }
87
+ );
88
+ }
89
+ return current;
90
+ }
91
+
92
+ function inspectReleaseSourceIntegration(sourceRevision, options = {}) {
93
+ assertNoMainlineOverride(options);
94
+ const cwd = path.resolve(options.cwd || process.cwd());
95
+ const source = normalizeSourceRevision(sourceRevision);
96
+ const currentRepository = readGitSourceBase(cwd, 'HEAD');
97
+ if (source.repo !== currentRepository.repo) {
98
+ throw releaseMainlineError(
99
+ 'RELEASE_SOURCE_REPOSITORY_MISMATCH',
100
+ '冻结的发布提交与当前 Git 仓库不一致,不能验证主分支回合状态'
101
+ );
102
+ }
103
+ ensureCommitAvailable(cwd, source.baseCommit);
104
+ const mainline = resolveAuthoritativeMainline({ cwd });
105
+ if (
106
+ source.remoteName !== mainline.remoteName ||
107
+ source.mainBranch !== mainline.branchName ||
108
+ source.remoteUrlHash !== mainline.remoteUrlHash
109
+ ) {
110
+ throw releaseMainlineError(
111
+ 'RELEASE_MAINLINE_TARGET_CHANGED',
112
+ `release 冻结的权威主线 ${source.remoteName}/${source.mainBranch} 与当前远端默认分支 ${mainline.remoteName}/${mainline.branchName} 不一致;禁止改 remote/默认分支绕过回合校验,请恢复原权威远端后重试`,
113
+ {
114
+ frozen: {
115
+ remoteName: source.remoteName,
116
+ remoteUrlHash: source.remoteUrlHash,
117
+ mainBranch: source.mainBranch,
118
+ },
119
+ current: mainline,
120
+ }
121
+ );
122
+ }
123
+ ensureCommitAvailable(
124
+ cwd,
125
+ mainline.tipCommit,
126
+ mainline.remoteName,
127
+ mainline.branchName
128
+ );
129
+ const integrated = isGitAncestor(
130
+ cwd,
131
+ source.baseCommit,
132
+ mainline.tipCommit,
133
+ {
134
+ remoteName: mainline.remoteName,
135
+ branchName: mainline.branchName,
136
+ }
137
+ );
138
+ return {
139
+ integrated,
140
+ repositoryId: source.repo,
141
+ sourceCommit: source.baseCommit,
142
+ sourceTreeHash: source.treeHash,
143
+ sourceBranch: source.branch || null,
144
+ remoteName: mainline.remoteName,
145
+ remoteUrlHash: mainline.remoteUrlHash,
146
+ mainBranch: mainline.branchName,
147
+ mainTipCommit: mainline.tipCommit,
148
+ mainlineSource: mainline.source,
149
+ verifiedAt: new Date().toISOString(),
150
+ };
151
+ }
152
+
153
+ function assertReleaseSourceIntegrated(sourceRevision, options = {}) {
154
+ const status = inspectReleaseSourceIntegration(sourceRevision, options);
155
+ if (status.integrated) return status;
156
+ throw releaseMainlineError(
157
+ 'RELEASE_MAIN_MERGE_PENDING',
158
+ `已发布提交 ${shortCommit(status.sourceCommit)} 尚未以保留提交血缘的方式合并并推送到 ${formatMainline(status)}@${shortCommit(status.mainTipCommit)};请完成 merge/push 后重试 release end(squash/rebase 不保留发布提交,不能通过)`,
159
+ status
160
+ );
161
+ }
162
+
163
+ function resolveAuthoritativeMainline(options = {}) {
164
+ assertNoMainlineOverride(options);
165
+ const cwd = path.resolve(options.cwd || process.cwd());
166
+ const remotes = listLines(
167
+ readGit(cwd, ['remote'], { allowFailure: true }).stdout
168
+ );
169
+ const remoteName =
170
+ (remotes.includes('origin')
171
+ ? 'origin'
172
+ : remotes.length === 1
173
+ ? remotes[0]
174
+ : null);
175
+
176
+ if (remoteName) {
177
+ return resolveRemoteMainline({ cwd, remoteName });
178
+ }
179
+ if (remotes.length > 1) {
180
+ throw releaseMainlineError(
181
+ 'RELEASE_GIT_REMOTE_UNRESOLVED',
182
+ `存在多个 Git remote(${remotes.join(', ')})且没有 origin;请把权威远端规范为 origin 后重试`
183
+ );
184
+ }
185
+ throw releaseMainlineError(
186
+ 'RELEASE_GIT_REMOTE_REQUIRED',
187
+ '正式发布必须配置可在线验证的权威 Git remote(默认使用 origin);仅有本地 main/master 不能证明已推送主分支'
188
+ );
189
+ }
190
+
191
+ function resolveRemoteMainline({ cwd, remoteName }) {
192
+ const remoteUrlHash = readRemoteUrlHash(cwd, remoteName);
193
+ const remoteHead = readGit(
194
+ cwd,
195
+ ['ls-remote', '--symref', remoteName, 'HEAD'],
196
+ { allowFailure: true }
197
+ );
198
+ if (remoteHead.ok) {
199
+ const parsed = parseRemoteHead(remoteHead.stdout);
200
+ if (parsed) {
201
+ assertSafeDefaultBranch(remoteName, parsed.branchName);
202
+ return {
203
+ remoteName,
204
+ remoteUrlHash,
205
+ branchName: parsed.branchName,
206
+ tipCommit: parsed.tipCommit,
207
+ source: 'remote-head',
208
+ };
209
+ }
210
+ }
211
+
212
+ const candidates = ['main', 'master']
213
+ .map(candidate => ({
214
+ branchName: candidate,
215
+ tipCommit: readRemoteBranchTip(cwd, remoteName, candidate, {
216
+ optional: true,
217
+ }),
218
+ }))
219
+ .filter(candidate => candidate.tipCommit);
220
+ if (candidates.length === 1) {
221
+ return {
222
+ remoteName,
223
+ remoteUrlHash,
224
+ ...candidates[0],
225
+ source: 'remote-main-master-fallback',
226
+ };
227
+ }
228
+ if (candidates.length > 1) {
229
+ throw releaseMainlineError(
230
+ 'RELEASE_MAIN_BRANCH_AMBIGUOUS',
231
+ `远端 ${remoteName} 同时存在 main 与 master 且无法读取 remote HEAD;请先修复远端默认分支 HEAD`
232
+ );
233
+ }
234
+ const detail = remoteHead.stderr
235
+ ? `: ${sanitizeGitError(remoteHead.stderr)}`
236
+ : '';
237
+ throw releaseMainlineError(
238
+ 'RELEASE_MAIN_BRANCH_UNRESOLVED',
239
+ `无法解析远端 ${remoteName} 的权威主分支${detail}`
240
+ );
241
+ }
242
+
243
+ function readRemoteBranchTip(cwd, remoteName, branchName, options = {}) {
244
+ const result = readGit(
245
+ cwd,
246
+ ['ls-remote', remoteName, `refs/heads/${branchName}`],
247
+ { allowFailure: true }
248
+ );
249
+ if (!result.ok) {
250
+ if (options.optional) return null;
251
+ throw releaseMainlineError(
252
+ 'RELEASE_MAIN_REF_UNVERIFIED',
253
+ `无法读取 ${remoteName}/${branchName}: ${sanitizeGitError(result.stderr)}`
254
+ );
255
+ }
256
+ const line = listLines(result.stdout)[0] || '';
257
+ const tipCommit = line.split(/\s+/)[0] || '';
258
+ if (!isObjectId(tipCommit)) {
259
+ if (options.optional) return null;
260
+ throw releaseMainlineError(
261
+ 'RELEASE_MAIN_REF_UNVERIFIED',
262
+ `远端主分支不存在: ${remoteName}/${branchName}`
263
+ );
264
+ }
265
+ return tipCommit.toLowerCase();
266
+ }
267
+
268
+ function parseRemoteHead(output) {
269
+ const lines = listLines(output);
270
+ const refLine = lines.find(
271
+ line => line.startsWith('ref: refs/heads/') && /\sHEAD$/.test(line)
272
+ );
273
+ const tipLine = lines.find(line =>
274
+ /^[0-9a-f]{40,64}\s+HEAD$/i.test(line)
275
+ );
276
+ if (!refLine || !tipLine) return null;
277
+ const branchName = refLine
278
+ .replace(/^ref: refs\/heads\//, '')
279
+ .replace(/\s+HEAD$/, '');
280
+ const tipCommit = tipLine.split(/\s+/)[0].toLowerCase();
281
+ if (!branchName || !isObjectId(tipCommit)) return null;
282
+ return { branchName, tipCommit };
283
+ }
284
+
285
+ function ensureCommitAvailable(cwd, commit, remoteName, branchName) {
286
+ const exists = readGit(cwd, ['cat-file', '-e', `${commit}^{commit}`], {
287
+ allowFailure: true,
288
+ });
289
+ if (exists.ok) return;
290
+ if (remoteName) {
291
+ const fetched = readGit(
292
+ cwd,
293
+ [
294
+ 'fetch',
295
+ '--quiet',
296
+ '--no-tags',
297
+ remoteName,
298
+ branchName ? `refs/heads/${branchName}` : commit,
299
+ ],
300
+ { allowFailure: true }
301
+ );
302
+ if (
303
+ fetched.ok &&
304
+ readGit(cwd, ['cat-file', '-e', `${commit}^{commit}`], {
305
+ allowFailure: true,
306
+ }).ok
307
+ ) {
308
+ return;
309
+ }
310
+ }
311
+ throw releaseMainlineError(
312
+ 'RELEASE_MAIN_REF_UNVERIFIED',
313
+ `本地缺少提交 ${shortCommit(commit)},无法验证主分支血缘;请 fetch 完整历史后重试`
314
+ );
315
+ }
316
+
317
+ function isGitAncestor(cwd, ancestorCommit, descendantCommit, options = {}) {
318
+ let result = readGit(
319
+ cwd,
320
+ ['merge-base', '--is-ancestor', ancestorCommit, descendantCommit],
321
+ { allowFailure: true }
322
+ );
323
+ if (result.status === 0) return true;
324
+ if (result.status !== 1) {
325
+ throw releaseMainlineError(
326
+ 'RELEASE_GIT_HISTORY_UNVERIFIED',
327
+ `无法验证提交血缘: ${sanitizeGitError(result.stderr) || `git merge-base 退出 ${result.status}`}`
328
+ );
329
+ }
330
+
331
+ const shallow = readGit(cwd, ['rev-parse', '--is-shallow-repository'], {
332
+ allowFailure: true,
333
+ }).stdout === 'true';
334
+ if (!shallow) return false;
335
+ if (options.remoteName && options.branchName) {
336
+ readGit(
337
+ cwd,
338
+ [
339
+ 'fetch',
340
+ '--quiet',
341
+ '--no-tags',
342
+ '--deepen=1000',
343
+ options.remoteName,
344
+ `refs/heads/${options.branchName}`,
345
+ ],
346
+ { allowFailure: true }
347
+ );
348
+ result = readGit(
349
+ cwd,
350
+ ['merge-base', '--is-ancestor', ancestorCommit, descendantCommit],
351
+ { allowFailure: true }
352
+ );
353
+ if (result.status === 0) return true;
354
+ if (result.status !== 1) {
355
+ throw releaseMainlineError(
356
+ 'RELEASE_GIT_HISTORY_UNVERIFIED',
357
+ `加深历史后仍无法验证提交血缘: ${sanitizeGitError(result.stderr) || `git merge-base 退出 ${result.status}`}`
358
+ );
359
+ }
360
+ }
361
+ throw releaseMainlineError(
362
+ 'RELEASE_GIT_HISTORY_UNVERIFIED',
363
+ '当前为浅克隆且历史不足,无法可靠判断发布提交与主分支血缘;请 fetch 完整历史后重试'
364
+ );
365
+ }
366
+
367
+ function readCurrentBranch(cwd) {
368
+ return readGit(cwd, ['branch', '--show-current'], {
369
+ allowFailure: true,
370
+ }).stdout;
371
+ }
372
+
373
+ function readGit(cwd, args, options = {}) {
374
+ const result = spawnSync('git', ['-C', cwd, ...args], {
375
+ encoding: 'utf8',
376
+ maxBuffer: GIT_OUTPUT_LIMIT,
377
+ timeout: 30_000,
378
+ env: {
379
+ ...process.env,
380
+ GIT_TERMINAL_PROMPT: '0',
381
+ },
382
+ });
383
+ const stdout = String(result.stdout || '').trim();
384
+ const stderr = String(result.stderr || '').trim();
385
+ const ok = !result.error && result.status === 0;
386
+ if (!ok && !options.allowFailure) {
387
+ throw releaseMainlineError(
388
+ 'RELEASE_GIT_COMMAND_FAILED',
389
+ `Git 校验失败: git ${args.join(' ')}${stderr ? `: ${sanitizeGitError(stderr)}` : ''}`
390
+ );
391
+ }
392
+ return { ok, stdout, stderr, status: result.status, error: result.error };
393
+ }
394
+
395
+ function normalizeSourceRevision(value) {
396
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
397
+ throw releaseMainlineError(
398
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED',
399
+ '发布会话缺少冻结的 releaseSourceRevision;禁止无主线证据结束,请先人工核对线上状态'
400
+ );
401
+ }
402
+ const repo = String(value.repo || value.repositoryId || '').trim();
403
+ const baseCommit = String(value.baseCommit || value.commit || '')
404
+ .trim()
405
+ .toLowerCase();
406
+ const treeHash = String(value.treeHash || '').trim().toLowerCase();
407
+ const remoteName = normalizeOptionalName(value.remoteName, 'remote');
408
+ const mainBranch = String(value.mainBranch || '').trim();
409
+ const remoteUrlHash = String(value.remoteUrlHash || '').trim().toLowerCase();
410
+ if (
411
+ !/^sha256:[0-9a-f]{64}$/i.test(repo) ||
412
+ !isObjectId(baseCommit) ||
413
+ !isObjectId(treeHash) ||
414
+ !remoteName ||
415
+ !['main', 'master'].includes(mainBranch) ||
416
+ !/^sha256:[0-9a-f]{64}$/.test(remoteUrlHash)
417
+ ) {
418
+ throw releaseMainlineError(
419
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED',
420
+ 'releaseSourceRevision 缺少有效的 repo/baseCommit/treeHash/remoteName/remoteUrlHash/mainBranch'
421
+ );
422
+ }
423
+ return {
424
+ ...value,
425
+ repo,
426
+ repositoryId: repo,
427
+ baseCommit,
428
+ treeHash,
429
+ remoteName,
430
+ remoteUrlHash,
431
+ mainBranch,
432
+ };
433
+ }
434
+
435
+ function readRemoteUrlHash(cwd, remoteName) {
436
+ const result = readGit(cwd, ['remote', 'get-url', remoteName]);
437
+ if (!result.stdout) {
438
+ throw releaseMainlineError(
439
+ 'RELEASE_GIT_REMOTE_UNRESOLVED',
440
+ `无法读取权威远端地址: ${remoteName}`
441
+ );
442
+ }
443
+ const identity = normalizeRemoteUrlIdentity(result.stdout);
444
+ return `sha256:${crypto.createHash('sha256').update(identity).digest('hex')}`;
445
+ }
446
+
447
+ function normalizeRemoteUrlIdentity(value) {
448
+ const remote = String(value || '').trim();
449
+ try {
450
+ const url = new URL(remote);
451
+ url.username = '';
452
+ url.password = '';
453
+ url.search = '';
454
+ url.hash = '';
455
+ return url.toString();
456
+ } catch {
457
+ return remote
458
+ .replace(/^[^@/]+@([^:]+):/, '$1:')
459
+ .replace(/[?#].*$/, '');
460
+ }
461
+ }
462
+
463
+ function assertSafeDefaultBranch(remoteName, branchName) {
464
+ if (['main', 'master'].includes(branchName)) return;
465
+ throw releaseMainlineError(
466
+ 'RELEASE_MAIN_BRANCH_UNSAFE',
467
+ `远端 ${remoteName} 的默认分支为 ${branchName},不是受支持的 main/master;请先修复远端默认分支,不能用 feature branch 代替主线`
468
+ );
469
+ }
470
+
471
+ function assertNoMainlineOverride(options = {}) {
472
+ if (
473
+ options.remoteName ||
474
+ options.branchName ||
475
+ options.mainlineRemote ||
476
+ options.mainlineBranch
477
+ ) {
478
+ throw releaseMainlineError(
479
+ 'RELEASE_MAINLINE_OVERRIDE_FORBIDDEN',
480
+ '权威主线只能来自 Git remote 的实时默认分支;禁止通过参数或环境变量改写'
481
+ );
482
+ }
483
+ }
484
+
485
+ function normalizeOptionalName(value, label) {
486
+ const text = String(value || '').trim();
487
+ if (!text) return null;
488
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(text)) {
489
+ throw releaseMainlineError(
490
+ 'RELEASE_GIT_REF_INVALID',
491
+ `非法 ${label}: ${text}`
492
+ );
493
+ }
494
+ return text;
495
+ }
496
+
497
+ function releaseMainlineError(code, message, details) {
498
+ const error = new Error(`${code}: ${message}`);
499
+ error.code = code;
500
+ if (details !== undefined) error.details = details;
501
+ return error;
502
+ }
503
+
504
+ function sanitizeGitError(value) {
505
+ return String(value || '')
506
+ .replace(/https?:\/\/[^\s/@]+@/gi, 'https://***@')
507
+ .replace(/\s+/g, ' ')
508
+ .slice(0, 500);
509
+ }
510
+
511
+ function summarizePaths(paths, limit = 8) {
512
+ const selected = paths.slice(0, limit).join(', ');
513
+ return paths.length > limit
514
+ ? `${selected}(另有 ${paths.length - limit} 项)`
515
+ : selected;
516
+ }
517
+
518
+ function shortCommit(value) {
519
+ return String(value || '').slice(0, 12) || '-';
520
+ }
521
+
522
+ function formatMainline(value) {
523
+ return value.remoteName
524
+ ? `${value.remoteName}/${value.branchName || value.mainBranch}`
525
+ : value.branchName || value.mainBranch;
526
+ }
527
+
528
+ function listLines(value) {
529
+ return String(value || '')
530
+ .split(/\r?\n/)
531
+ .map(line => line.trim())
532
+ .filter(Boolean);
533
+ }
534
+
535
+ function isObjectId(value) {
536
+ return /^[0-9a-f]{40,64}$/i.test(String(value || ''));
537
+ }
538
+
539
+ module.exports = {
540
+ assertReleaseSourceIntegrated,
541
+ assertReleaseSourceRevisionStable,
542
+ inspectReleaseSourceIntegration,
543
+ prepareReleaseSourceRevision,
544
+ resolveAuthoritativeMainline,
545
+ };
@@ -54,18 +54,7 @@ function prepareRuntimeDeployLineage(options = {}) {
54
54
  );
55
55
  }
56
56
 
57
- const dirtyPaths = listRuntimeBuildInputChanges(cwd);
58
- if (dirtyPaths.length > 0) {
59
- throw runtimeLineageError(
60
- 'RUNTIME_SOURCE_DIRTY',
61
- [
62
- '创建 Runtime release 前,所有可能进入构建的源码和配置必须提交到当前 HEAD。',
63
- `未提交路径: ${summarizePaths(dirtyPaths)}`,
64
- '请先提交/合并这些修改并重新构建;.openxiangda/、dist/ 等纯状态或生成目录不会触发此阻断。',
65
- ].join(' '),
66
- { dirtyPaths }
67
- );
68
- }
57
+ assertRuntimeSourceClean(cwd);
69
58
 
70
59
  const legacyBootstrap = Boolean(parentReleaseId && !parentSourceRevision);
71
60
  if (parentSourceRevision) {
@@ -111,6 +100,23 @@ function prepareRuntimeDeployLineage(options = {}) {
111
100
  });
112
101
  }
113
102
 
103
+ function assertRuntimeSourceClean(cwd = process.cwd()) {
104
+ const root = path.resolve(cwd);
105
+ const dirtyPaths = listRuntimeBuildInputChanges(root);
106
+ if (dirtyPaths.length > 0) {
107
+ throw runtimeLineageError(
108
+ 'RUNTIME_SOURCE_DIRTY',
109
+ [
110
+ '创建 Runtime release 前,所有可能进入构建的源码和配置必须提交到当前 HEAD。',
111
+ `未提交路径: ${summarizePaths(dirtyPaths)}`,
112
+ '请先提交/合并这些修改并重新构建;.openxiangda/、dist/ 等纯状态或生成目录不会触发此阻断。',
113
+ ].join(' '),
114
+ { dirtyPaths }
115
+ );
116
+ }
117
+ return true;
118
+ }
119
+
114
120
  function resolveRuntimeRollbackBypass(options = {}) {
115
121
  const enabled = Boolean(options.allowRollback);
116
122
  const reason = String(options.reason || '').trim();
@@ -289,6 +295,7 @@ function runtimeLineageError(code, message, details = {}) {
289
295
 
290
296
  module.exports = {
291
297
  MIN_ROLLBACK_REASON_LENGTH,
298
+ assertRuntimeSourceClean,
292
299
  isPureRuntimeOutputPath,
293
300
  listRuntimeBuildInputChanges,
294
301
  normalizeRuntimeSourceRevision,