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.
- package/README.md +6 -2
- package/lib/change-baseline.js +88 -2
- package/lib/cli.js +565 -73
- package/lib/js-code-build.js +120 -8
- package/lib/publish-lease.js +13 -1
- package/lib/release-mainline.js +545 -0
- package/lib/runtime-lineage.js +19 -12
- package/lib/sdd.js +137 -0
- package/lib/workspace-bootstrap.js +36 -7
- package/lib/workspace-init.js +12 -3
- package/lib/worktree-owner.js +13 -0
- package/openxiangda-skills/SKILL.md +9 -3
- package/openxiangda-skills/references/automation-v3.md +1 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
- package/openxiangda-skills/references/workflow-v3.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -2
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
- package/package.json +3 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +2 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -2
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +2 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
- package/templates/openxiangda-react-spa/AGENTS.md +8 -6
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +172 -20
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +19 -39
- package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +4 -5
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +3 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +3 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +4 -4
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +172 -20
- 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
|
+
};
|
package/lib/runtime-lineage.js
CHANGED
|
@@ -54,18 +54,7 @@ function prepareRuntimeDeployLineage(options = {}) {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
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,
|