openxiangda 1.0.159 → 1.0.161
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 +39 -13
- package/lib/change-baseline.js +1077 -0
- package/lib/cli.js +12002 -6723
- package/lib/config.js +272 -7
- package/lib/http.js +2 -0
- package/lib/js-code-build.js +61 -0
- package/lib/publish-context.js +98 -0
- package/lib/publish-lease-heartbeat.js +182 -0
- package/lib/publish-lease.js +190 -0
- package/lib/release-telemetry.js +182 -0
- package/lib/runtime-lineage.js +297 -0
- package/lib/sdd.js +567 -35
- package/lib/source-dependencies.js +742 -0
- package/lib/workspace-init.js +6 -1
- package/lib/worktree-owner.js +219 -0
- package/openxiangda-skills/SKILL.md +103 -228
- package/openxiangda-skills/references/architecture-design.md +2 -1
- package/openxiangda-skills/references/openxiangda-api.md +27 -0
- package/openxiangda-skills/references/pages/page-sdk.md +11 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/openxiangda-skills/references/workspace-state.md +2 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
- package/package.json +20 -1
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
- package/packages/sdk/dist/components/index.cjs +29 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +29 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
- package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
- package/packages/sdk/dist/runtime/index.cjs +150 -22
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +150 -22
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +121 -21
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +15 -2
- package/packages/sdk/dist/runtime/react.d.ts +15 -2
- package/packages/sdk/dist/runtime/react.mjs +121 -21
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
- package/templates/openxiangda-react-spa/AGENTS.md +25 -12
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +344 -60
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { execFileSync } = require('child_process');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { saveProjectState } = require('./config');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS = 1800;
|
|
8
|
+
const PUBLISH_LEASE_RENEW_WINDOW_SECONDS = 180;
|
|
9
|
+
|
|
10
|
+
function getStoredPublishLease(target) {
|
|
11
|
+
const lease = target?.bound?.promotion?.publishLease;
|
|
12
|
+
return lease && typeof lease === 'object' ? lease : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getUsableStoredPublishLease(target, now = Date.now()) {
|
|
16
|
+
const lease = getStoredPublishLease(target);
|
|
17
|
+
if (!lease?.leaseId || !lease?.expiresAt) return null;
|
|
18
|
+
const expiresAt = new Date(lease.expiresAt).getTime();
|
|
19
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Number(now)) return null;
|
|
20
|
+
if (lease.appType && lease.appType !== target.appType) return null;
|
|
21
|
+
if (lease.profile && lease.profile !== target.profileName) return null;
|
|
22
|
+
if (!isPublishLeaseOwnedByCurrentSession(lease)) return null;
|
|
23
|
+
return lease;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isPublishLeaseExpired(lease, now = Date.now()) {
|
|
27
|
+
if (!lease?.expiresAt) return true;
|
|
28
|
+
const expiresAt = new Date(lease.expiresAt).getTime();
|
|
29
|
+
return !Number.isFinite(expiresAt) || expiresAt <= Number(now);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function currentCodexThreadId(env = process.env) {
|
|
33
|
+
return String(env?.CODEX_THREAD_ID || '').trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createPublishClientSessionId(env = process.env) {
|
|
37
|
+
const threadId = currentCodexThreadId(env);
|
|
38
|
+
return threadId ? `codex:${threadId}` : `cli:${crypto.randomUUID()}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isPublishLeaseOwnedByCurrentSession(lease, env = process.env) {
|
|
42
|
+
const ownerThreadId = String(lease?.workspace?.threadId || '').trim();
|
|
43
|
+
const threadId = currentCodexThreadId(env);
|
|
44
|
+
if (ownerThreadId && threadId) return ownerThreadId === threadId;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldRenewPublishLease(
|
|
49
|
+
lease,
|
|
50
|
+
now = Date.now(),
|
|
51
|
+
renewWindowSeconds = PUBLISH_LEASE_RENEW_WINDOW_SECONDS
|
|
52
|
+
) {
|
|
53
|
+
if (!lease?.expiresAt) return true;
|
|
54
|
+
const expiresAt = new Date(lease.expiresAt).getTime();
|
|
55
|
+
if (!Number.isFinite(expiresAt)) return true;
|
|
56
|
+
return expiresAt - Number(now) <= Number(renewWindowSeconds) * 1000;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function savePublishLease(target, input, options = {}) {
|
|
60
|
+
const lease = normalizePublishLease(input, target);
|
|
61
|
+
target.bound.promotion = {
|
|
62
|
+
...(target.bound.promotion || {}),
|
|
63
|
+
publishLease: lease,
|
|
64
|
+
};
|
|
65
|
+
target.bound.updatedAt = new Date().toISOString();
|
|
66
|
+
if (target.state.profiles?.[target.profileName]) {
|
|
67
|
+
target.state.profiles[target.profileName] = target.bound;
|
|
68
|
+
}
|
|
69
|
+
(options.saveState || saveProjectState)(target.state, options.cwd || process.cwd());
|
|
70
|
+
return lease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function clearPublishLease(target, leaseId, options = {}) {
|
|
74
|
+
const current = getStoredPublishLease(target);
|
|
75
|
+
if (!current) return false;
|
|
76
|
+
if (leaseId && current.leaseId !== leaseId) return false;
|
|
77
|
+
target.bound.promotion = { ...(target.bound.promotion || {}) };
|
|
78
|
+
delete target.bound.promotion.publishLease;
|
|
79
|
+
if (Object.keys(target.bound.promotion).length === 0) {
|
|
80
|
+
delete target.bound.promotion;
|
|
81
|
+
}
|
|
82
|
+
target.bound.updatedAt = new Date().toISOString();
|
|
83
|
+
if (target.state.profiles?.[target.profileName]) {
|
|
84
|
+
target.state.profiles[target.profileName] = target.bound;
|
|
85
|
+
}
|
|
86
|
+
(options.saveState || saveProjectState)(target.state, options.cwd || process.cwd());
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizePublishLease(input, target) {
|
|
91
|
+
const leaseId = String(input?.leaseId || '').trim();
|
|
92
|
+
const expiresAt = normalizeDate(input?.expiresAt, 'expiresAt');
|
|
93
|
+
if (!leaseId) throw new Error('publish lease response 缺少 leaseId');
|
|
94
|
+
return {
|
|
95
|
+
leaseId,
|
|
96
|
+
appType: String(input?.appType || target?.appType || '').trim(),
|
|
97
|
+
profile: String(target?.profileName || '').trim(),
|
|
98
|
+
changeId: String(input?.changeId || '').trim() || null,
|
|
99
|
+
clientSessionId: String(input?.clientSessionId || '').trim() || null,
|
|
100
|
+
baseRevision: String(input?.baseRevision || '').trim() || null,
|
|
101
|
+
expiresAt,
|
|
102
|
+
holder: input?.holder === 'other' ? 'other' : 'self',
|
|
103
|
+
workspace: readWorkspaceIdentity(optionsCwd(target)),
|
|
104
|
+
updatedAt: new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildPublishBaseRevision(snapshot) {
|
|
109
|
+
const app = unwrapRecord(snapshot?.app) || {};
|
|
110
|
+
const runtime = unwrapRecord(snapshot?.runtime) || {};
|
|
111
|
+
const updatedAt = textValue(
|
|
112
|
+
app.updatedAt || runtime.updatedAt || snapshot?.updatedAt
|
|
113
|
+
);
|
|
114
|
+
const activeReleaseId = textValue(
|
|
115
|
+
app.activeRuntimeReleaseId ||
|
|
116
|
+
runtime.activeReleaseId ||
|
|
117
|
+
snapshot?.activeRuntimeReleaseId
|
|
118
|
+
);
|
|
119
|
+
const activeBuildId = textValue(
|
|
120
|
+
app.activeRuntimeBuildId || runtime.activeBuildId || snapshot?.activeRuntimeBuildId
|
|
121
|
+
);
|
|
122
|
+
return [
|
|
123
|
+
`appUpdatedAt=${updatedAt || '-'}`,
|
|
124
|
+
`runtimeRelease=${activeReleaseId || '-'}`,
|
|
125
|
+
`runtimeBuild=${activeBuildId || '-'}`,
|
|
126
|
+
].join(';');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readWorkspaceIdentity(cwd = process.cwd()) {
|
|
130
|
+
const resolvedCwd = path.resolve(cwd);
|
|
131
|
+
return {
|
|
132
|
+
cwd: resolvedCwd,
|
|
133
|
+
branch: readGitValue(resolvedCwd, ['branch', '--show-current']),
|
|
134
|
+
commit: readGitValue(resolvedCwd, ['rev-parse', 'HEAD']),
|
|
135
|
+
worktreeRoot: readGitValue(resolvedCwd, ['rev-parse', '--show-toplevel']),
|
|
136
|
+
threadId: currentCodexThreadId(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function readGitValue(cwd, args) {
|
|
141
|
+
try {
|
|
142
|
+
return String(
|
|
143
|
+
execFileSync('git', ['-C', cwd, ...args], {
|
|
144
|
+
encoding: 'utf8',
|
|
145
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
146
|
+
})
|
|
147
|
+
).trim();
|
|
148
|
+
} catch {
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function optionsCwd(target) {
|
|
154
|
+
return target?.cwd || process.cwd();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeDate(value, field) {
|
|
158
|
+
const date = new Date(value);
|
|
159
|
+
if (Number.isNaN(date.getTime())) {
|
|
160
|
+
throw new Error(`publish lease response ${field} 无效`);
|
|
161
|
+
}
|
|
162
|
+
return date.toISOString();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function unwrapRecord(value) {
|
|
166
|
+
if (!value || typeof value !== 'object') return null;
|
|
167
|
+
if (value.data && typeof value.data === 'object') return value.data;
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function textValue(value) {
|
|
172
|
+
if (value === undefined || value === null) return '';
|
|
173
|
+
return String(value).trim();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS,
|
|
178
|
+
PUBLISH_LEASE_RENEW_WINDOW_SECONDS,
|
|
179
|
+
buildPublishBaseRevision,
|
|
180
|
+
clearPublishLease,
|
|
181
|
+
createPublishClientSessionId,
|
|
182
|
+
currentCodexThreadId,
|
|
183
|
+
getStoredPublishLease,
|
|
184
|
+
getUsableStoredPublishLease,
|
|
185
|
+
isPublishLeaseExpired,
|
|
186
|
+
isPublishLeaseOwnedByCurrentSession,
|
|
187
|
+
readWorkspaceIdentity,
|
|
188
|
+
savePublishLease,
|
|
189
|
+
shouldRenewPublishLease,
|
|
190
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const DEFAULT_PROGRESS_HEARTBEAT_MS = 15_000;
|
|
2
|
+
|
|
3
|
+
class ReleaseTelemetry {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
this.operation = String(options.operation || 'release').trim() || 'release';
|
|
6
|
+
this.now = options.now || Date.now;
|
|
7
|
+
this.onProgress =
|
|
8
|
+
typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
9
|
+
this.heartbeatMs = normalizePositiveInteger(
|
|
10
|
+
options.heartbeatMs,
|
|
11
|
+
DEFAULT_PROGRESS_HEARTBEAT_MS
|
|
12
|
+
);
|
|
13
|
+
this.setInterval = options.setInterval || global.setInterval;
|
|
14
|
+
this.clearInterval = options.clearInterval || global.clearInterval;
|
|
15
|
+
this.startedAtMs = Number(this.now());
|
|
16
|
+
this.finishedAtMs = null;
|
|
17
|
+
this.phases = [];
|
|
18
|
+
this.active = [];
|
|
19
|
+
this.timer = null;
|
|
20
|
+
this.stopped = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
start() {
|
|
24
|
+
if (this.stopped || this.timer) return this;
|
|
25
|
+
this.timer = this.setInterval(() => this.emitHeartbeat(), this.heartbeatMs);
|
|
26
|
+
this.timer?.unref?.();
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async runPhase(name, metadata, operation) {
|
|
31
|
+
if (typeof metadata === 'function') {
|
|
32
|
+
operation = metadata;
|
|
33
|
+
metadata = {};
|
|
34
|
+
}
|
|
35
|
+
if (typeof operation !== 'function') {
|
|
36
|
+
throw new TypeError('release telemetry phase operation 必须是函数');
|
|
37
|
+
}
|
|
38
|
+
const phase = this.beginPhase(name, metadata);
|
|
39
|
+
try {
|
|
40
|
+
const value = await operation();
|
|
41
|
+
this.endPhase(phase, 'success');
|
|
42
|
+
return value;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
this.endPhase(phase, 'failed', {
|
|
45
|
+
errorCode: error?.code || error?.errorCode || null,
|
|
46
|
+
errorMessage: String(error?.message || error || '').slice(0, 500),
|
|
47
|
+
});
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
beginPhase(name, metadata = {}) {
|
|
53
|
+
const phase = {
|
|
54
|
+
name: normalizeName(name),
|
|
55
|
+
status: 'running',
|
|
56
|
+
startedAtMs: Number(this.now()),
|
|
57
|
+
finishedAtMs: null,
|
|
58
|
+
durationMs: null,
|
|
59
|
+
metadata: normalizeMetadata(metadata),
|
|
60
|
+
};
|
|
61
|
+
this.phases.push(phase);
|
|
62
|
+
this.active.push(phase);
|
|
63
|
+
this.emit({ type: 'phase-start', phase: this.serializePhase(phase) });
|
|
64
|
+
return phase;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
endPhase(phase, status = 'success', metadata = {}) {
|
|
68
|
+
if (!phase || phase.status !== 'running') return phase;
|
|
69
|
+
phase.finishedAtMs = Number(this.now());
|
|
70
|
+
phase.durationMs = Math.max(0, phase.finishedAtMs - phase.startedAtMs);
|
|
71
|
+
phase.status = status;
|
|
72
|
+
phase.metadata = {
|
|
73
|
+
...phase.metadata,
|
|
74
|
+
...normalizeMetadata(metadata),
|
|
75
|
+
};
|
|
76
|
+
const index = this.active.lastIndexOf(phase);
|
|
77
|
+
if (index >= 0) this.active.splice(index, 1);
|
|
78
|
+
this.emit({ type: 'phase-end', phase: this.serializePhase(phase) });
|
|
79
|
+
return phase;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addMetadata(phase, metadata = {}) {
|
|
83
|
+
if (!phase) return;
|
|
84
|
+
phase.metadata = {
|
|
85
|
+
...phase.metadata,
|
|
86
|
+
...normalizeMetadata(metadata),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
emitHeartbeat() {
|
|
91
|
+
if (this.stopped || this.active.length === 0) return;
|
|
92
|
+
const now = Number(this.now());
|
|
93
|
+
const phase = this.active[this.active.length - 1];
|
|
94
|
+
this.emit({
|
|
95
|
+
type: 'heartbeat',
|
|
96
|
+
phase: this.serializePhase(phase, now),
|
|
97
|
+
operationElapsedMs: Math.max(0, now - this.startedAtMs),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
snapshot() {
|
|
102
|
+
const now =
|
|
103
|
+
this.finishedAtMs === null ? Number(this.now()) : this.finishedAtMs;
|
|
104
|
+
return {
|
|
105
|
+
operation: this.operation,
|
|
106
|
+
startedAt: new Date(this.startedAtMs).toISOString(),
|
|
107
|
+
finishedAt:
|
|
108
|
+
this.finishedAtMs === null
|
|
109
|
+
? null
|
|
110
|
+
: new Date(this.finishedAtMs).toISOString(),
|
|
111
|
+
durationMs: Math.max(0, now - this.startedAtMs),
|
|
112
|
+
phases: this.phases.map(phase => this.serializePhase(phase, now)),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
stop() {
|
|
117
|
+
if (this.timer) {
|
|
118
|
+
this.clearInterval(this.timer);
|
|
119
|
+
this.timer = null;
|
|
120
|
+
}
|
|
121
|
+
if (!this.stopped) {
|
|
122
|
+
this.finishedAtMs = Number(this.now());
|
|
123
|
+
for (const phase of [...this.active]) {
|
|
124
|
+
this.endPhase(phase, 'interrupted');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.stopped = true;
|
|
128
|
+
return this.snapshot();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
serializePhase(phase, now = Number(this.now())) {
|
|
132
|
+
const finishedAtMs = phase.finishedAtMs;
|
|
133
|
+
return {
|
|
134
|
+
name: phase.name,
|
|
135
|
+
status: phase.status,
|
|
136
|
+
startedAt: new Date(phase.startedAtMs).toISOString(),
|
|
137
|
+
finishedAt:
|
|
138
|
+
finishedAtMs === null ? null : new Date(finishedAtMs).toISOString(),
|
|
139
|
+
durationMs:
|
|
140
|
+
phase.durationMs === null
|
|
141
|
+
? Math.max(0, now - phase.startedAtMs)
|
|
142
|
+
: phase.durationMs,
|
|
143
|
+
metadata: { ...phase.metadata },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
emit(event) {
|
|
148
|
+
try {
|
|
149
|
+
this.onProgress({ operation: this.operation, ...event });
|
|
150
|
+
} catch {
|
|
151
|
+
// Telemetry must never change release correctness.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeName(value) {
|
|
157
|
+
const name = String(value || '').trim();
|
|
158
|
+
if (!name) throw new TypeError('release telemetry phase name 不能为空');
|
|
159
|
+
return name.slice(0, 128);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeMetadata(value) {
|
|
163
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
164
|
+
return Object.fromEntries(
|
|
165
|
+
Object.entries(value).filter(([, item]) => item !== undefined)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizePositiveInteger(value, fallback) {
|
|
170
|
+
const numeric = Number(value);
|
|
171
|
+
return Number.isSafeInteger(numeric) && numeric > 0 ? numeric : fallback;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createReleaseTelemetry(options) {
|
|
175
|
+
return new ReleaseTelemetry(options);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
DEFAULT_PROGRESS_HEARTBEAT_MS,
|
|
180
|
+
ReleaseTelemetry,
|
|
181
|
+
createReleaseTelemetry,
|
|
182
|
+
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
const { readGitSourceBase } = require('./change-baseline');
|
|
5
|
+
|
|
6
|
+
const MIN_ROLLBACK_REASON_LENGTH = 8;
|
|
7
|
+
const PURE_RUNTIME_OUTPUT_PREFIXES = Object.freeze([
|
|
8
|
+
'.codegraph',
|
|
9
|
+
'.openxiangda',
|
|
10
|
+
'.turbo',
|
|
11
|
+
'.vite',
|
|
12
|
+
'coverage',
|
|
13
|
+
'dist',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'openspec',
|
|
16
|
+
]);
|
|
17
|
+
const IGNORED_BUILD_INPUT_PATHS = Object.freeze([
|
|
18
|
+
':(glob).env',
|
|
19
|
+
':(glob).env.*',
|
|
20
|
+
':(glob)app-workspace.config.*',
|
|
21
|
+
':(glob)components.json',
|
|
22
|
+
':(glob)index.html',
|
|
23
|
+
':(glob)package.json',
|
|
24
|
+
':(glob)package-lock.json',
|
|
25
|
+
':(glob)pnpm-lock.yaml',
|
|
26
|
+
':(glob)yarn.lock',
|
|
27
|
+
':(glob)postcss.config.*',
|
|
28
|
+
':(glob)public/**',
|
|
29
|
+
':(glob)scripts/**',
|
|
30
|
+
':(glob)src/**',
|
|
31
|
+
':(glob)tailwind.config.*',
|
|
32
|
+
':(glob)tsconfig*.json',
|
|
33
|
+
':(glob)vite.config.*',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function prepareRuntimeDeployLineage(options = {}) {
|
|
37
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
38
|
+
const app = normalizeAppSnapshot(options.app);
|
|
39
|
+
const activate = options.activate !== false;
|
|
40
|
+
const rollback = resolveRuntimeRollbackBypass(options);
|
|
41
|
+
const parentReleaseId = normalizeNullableString(app.activeRuntimeReleaseId);
|
|
42
|
+
const parentSourceRevision = normalizeRuntimeSourceRevision(
|
|
43
|
+
app.activeRuntimeSourceRevision,
|
|
44
|
+
{ optional: true }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
let sourceRevision;
|
|
48
|
+
try {
|
|
49
|
+
sourceRevision = normalizeRuntimeSourceRevision(readGitSourceBase(cwd, 'HEAD'));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw runtimeLineageError(
|
|
52
|
+
'RUNTIME_SOURCE_LINEAGE_REQUIRED',
|
|
53
|
+
`创建 Runtime release 前必须在可审计 Git 工作区的已提交 HEAD 上构建;当前目录无法读取 Git HEAD。${formatCause(error)}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
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
|
+
}
|
|
69
|
+
|
|
70
|
+
const legacyBootstrap = Boolean(parentReleaseId && !parentSourceRevision);
|
|
71
|
+
if (parentSourceRevision) {
|
|
72
|
+
const sameRepository = parentSourceRevision.repo === sourceRevision.repo;
|
|
73
|
+
const descendsFromActive =
|
|
74
|
+
sameRepository &&
|
|
75
|
+
isGitAncestor(cwd, parentSourceRevision.baseCommit, sourceRevision.baseCommit);
|
|
76
|
+
if ((!sameRepository || !descendsFromActive) && !rollback.enabled) {
|
|
77
|
+
const detail = sameRepository
|
|
78
|
+
? `线上基线 ${shortCommit(parentSourceRevision.baseCommit)} 不是当前 HEAD ${shortCommit(sourceRevision.baseCommit)} 的祖先`
|
|
79
|
+
: '当前 Git 仓库与线上 Runtime 的源码仓库不一致';
|
|
80
|
+
throw runtimeLineageError(
|
|
81
|
+
'RUNTIME_SOURCE_BASE_DIVERGED',
|
|
82
|
+
`${detail};已在构建和上传前阻断。请合并线上 release head 后重试;确需审计式回退时使用 --allow-runtime-rollback --reason "至少 8 个字符的原因"。`,
|
|
83
|
+
{ sourceRevision, parentSourceRevision, parentReleaseId }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return stripUndefinedValues({
|
|
87
|
+
sourceRevision,
|
|
88
|
+
parentReleaseId,
|
|
89
|
+
parentSourceRevision,
|
|
90
|
+
lineageBypassReason: rollback.enabled ? rollback.reason : undefined,
|
|
91
|
+
lineageStatus:
|
|
92
|
+
rollback.enabled && (!sameRepository || !descendsFromActive)
|
|
93
|
+
? 'rollback-bypass'
|
|
94
|
+
: activate
|
|
95
|
+
? 'descendant'
|
|
96
|
+
: 'preview',
|
|
97
|
+
auditable: true,
|
|
98
|
+
activationAllowed: true,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return stripUndefinedValues({
|
|
103
|
+
sourceRevision,
|
|
104
|
+
parentReleaseId,
|
|
105
|
+
parentSourceRevision,
|
|
106
|
+
lineageBypassReason: rollback.enabled ? rollback.reason : undefined,
|
|
107
|
+
lineageStatus: legacyBootstrap ? 'legacy-bootstrap' : 'bootstrap',
|
|
108
|
+
legacyBootstrap,
|
|
109
|
+
auditable: true,
|
|
110
|
+
activationAllowed: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveRuntimeRollbackBypass(options = {}) {
|
|
115
|
+
const enabled = Boolean(options.allowRollback);
|
|
116
|
+
const reason = String(options.reason || '').trim();
|
|
117
|
+
if (!enabled) return { enabled: false, reason: '' };
|
|
118
|
+
if (reason.length < MIN_ROLLBACK_REASON_LENGTH) {
|
|
119
|
+
throw runtimeLineageError(
|
|
120
|
+
'RUNTIME_ROLLBACK_REASON_REQUIRED',
|
|
121
|
+
`--allow-runtime-rollback 必须同时提供至少 ${MIN_ROLLBACK_REASON_LENGTH} 个字符的 --reason,原因会写入发布审计`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return { enabled: true, reason };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function listRuntimeBuildInputChanges(cwd = process.cwd()) {
|
|
128
|
+
const changed = runGitNameList(cwd, [
|
|
129
|
+
'diff',
|
|
130
|
+
'--name-only',
|
|
131
|
+
'-z',
|
|
132
|
+
'--relative',
|
|
133
|
+
'HEAD',
|
|
134
|
+
'--',
|
|
135
|
+
'.',
|
|
136
|
+
]);
|
|
137
|
+
const untracked = runGitNameList(cwd, [
|
|
138
|
+
'ls-files',
|
|
139
|
+
'--others',
|
|
140
|
+
'--exclude-standard',
|
|
141
|
+
'-z',
|
|
142
|
+
'--',
|
|
143
|
+
'.',
|
|
144
|
+
]);
|
|
145
|
+
const ignoredInputs = runGitNameList(
|
|
146
|
+
cwd,
|
|
147
|
+
[
|
|
148
|
+
'ls-files',
|
|
149
|
+
'--others',
|
|
150
|
+
'--ignored',
|
|
151
|
+
'--exclude-standard',
|
|
152
|
+
'-z',
|
|
153
|
+
'--',
|
|
154
|
+
...IGNORED_BUILD_INPUT_PATHS,
|
|
155
|
+
],
|
|
156
|
+
{ allowFailure: true }
|
|
157
|
+
);
|
|
158
|
+
return [...new Set([...changed, ...untracked, ...ignoredInputs])]
|
|
159
|
+
.map(normalizeGitPath)
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.filter(filePath => !isPureRuntimeOutputPath(filePath))
|
|
162
|
+
.sort((left, right) => left.localeCompare(right));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isPureRuntimeOutputPath(filePath) {
|
|
166
|
+
const normalized = normalizeGitPath(filePath);
|
|
167
|
+
const firstSegment = normalized.split('/')[0];
|
|
168
|
+
return (
|
|
169
|
+
PURE_RUNTIME_OUTPUT_PREFIXES.includes(firstSegment) ||
|
|
170
|
+
/(^|\/)\.DS_Store$/.test(normalized) ||
|
|
171
|
+
/\.tsbuildinfo$/.test(normalized)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isGitAncestor(cwd, ancestorCommit, descendantCommit) {
|
|
176
|
+
const result = spawnSync(
|
|
177
|
+
'git',
|
|
178
|
+
['-C', cwd, 'merge-base', '--is-ancestor', ancestorCommit, descendantCommit],
|
|
179
|
+
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }
|
|
180
|
+
);
|
|
181
|
+
return !result.error && result.status === 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function runGitNameList(cwd, args, options = {}) {
|
|
185
|
+
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
186
|
+
encoding: 'buffer',
|
|
187
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
188
|
+
});
|
|
189
|
+
if (result.error || result.status !== 0) {
|
|
190
|
+
if (options.allowFailure) return [];
|
|
191
|
+
const detail = Buffer.isBuffer(result.stderr)
|
|
192
|
+
? result.stderr.toString('utf8').trim()
|
|
193
|
+
: String(result.stderr || '').trim();
|
|
194
|
+
throw runtimeLineageError(
|
|
195
|
+
'RUNTIME_SOURCE_LINEAGE_REQUIRED',
|
|
196
|
+
`无法检查 Runtime Git 工作区状态${detail ? `: ${detail}` : ''}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const stdout = Buffer.isBuffer(result.stdout)
|
|
200
|
+
? result.stdout
|
|
201
|
+
: Buffer.from(result.stdout || '');
|
|
202
|
+
return stdout
|
|
203
|
+
.toString('utf8')
|
|
204
|
+
.split('\0')
|
|
205
|
+
.filter(Boolean);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeRuntimeSourceRevision(value, options = {}) {
|
|
209
|
+
if (value === undefined || value === null || value === '') {
|
|
210
|
+
if (options.optional) return null;
|
|
211
|
+
throw runtimeLineageError(
|
|
212
|
+
'RUNTIME_SOURCE_LINEAGE_REQUIRED',
|
|
213
|
+
'Runtime source revision 缺失'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
let input = value;
|
|
217
|
+
if (typeof input === 'string') {
|
|
218
|
+
try {
|
|
219
|
+
input = JSON.parse(input);
|
|
220
|
+
} catch {
|
|
221
|
+
input = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
225
|
+
throw runtimeLineageError(
|
|
226
|
+
'RUNTIME_SOURCE_LINEAGE_REQUIRED',
|
|
227
|
+
'线上 Runtime source revision 无效'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const repo = String(input.repo || input.repositoryId || '').trim();
|
|
231
|
+
const baseCommit = String(input.baseCommit || '').trim().toLowerCase();
|
|
232
|
+
const treeHash = String(input.treeHash || '').trim().toLowerCase();
|
|
233
|
+
if (
|
|
234
|
+
!/^sha256:[0-9a-f]{64}$/i.test(repo) ||
|
|
235
|
+
!/^[0-9a-f]{40,64}$/.test(baseCommit) ||
|
|
236
|
+
!/^[0-9a-f]{40,64}$/.test(treeHash)
|
|
237
|
+
) {
|
|
238
|
+
throw runtimeLineageError(
|
|
239
|
+
'RUNTIME_SOURCE_LINEAGE_REQUIRED',
|
|
240
|
+
'线上 Runtime source revision 缺少有效的 repo/baseCommit/treeHash'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return { repo, baseCommit, treeHash };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function normalizeAppSnapshot(value) {
|
|
247
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
248
|
+
return value.app || value.application || value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeNullableString(value) {
|
|
252
|
+
const text = String(value === undefined || value === null ? '' : value).trim();
|
|
253
|
+
return text || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeGitPath(value) {
|
|
257
|
+
return String(value || '')
|
|
258
|
+
.replace(/\\/g, '/')
|
|
259
|
+
.replace(/^\.\//, '')
|
|
260
|
+
.replace(/^\/+|\/+$/g, '');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function summarizePaths(paths, limit = 8) {
|
|
264
|
+
const selected = paths.slice(0, limit).join(', ');
|
|
265
|
+
return paths.length > limit ? `${selected}(另有 ${paths.length - limit} 项)` : selected;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function shortCommit(value) {
|
|
269
|
+
return String(value || '').slice(0, 12) || '-';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatCause(error) {
|
|
273
|
+
const message = String(error?.message || '').trim();
|
|
274
|
+
return message ? ` 原因: ${message}` : '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stripUndefinedValues(value) {
|
|
278
|
+
return Object.fromEntries(
|
|
279
|
+
Object.entries(value).filter(([, item]) => item !== undefined)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function runtimeLineageError(code, message, details = {}) {
|
|
284
|
+
const error = new Error(`[${code}] ${message}`);
|
|
285
|
+
error.code = code;
|
|
286
|
+
Object.assign(error, details);
|
|
287
|
+
return error;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
MIN_ROLLBACK_REASON_LENGTH,
|
|
292
|
+
isPureRuntimeOutputPath,
|
|
293
|
+
listRuntimeBuildInputChanges,
|
|
294
|
+
normalizeRuntimeSourceRevision,
|
|
295
|
+
prepareRuntimeDeployLineage,
|
|
296
|
+
resolveRuntimeRollbackBypass,
|
|
297
|
+
};
|