openxiangda 1.0.172 → 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 +6 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +984 -125
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +59 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +246 -56
- package/openxiangda-skills/SKILL.md +8 -6
- package/openxiangda-skills/references/openxiangda-api.md +6 -6
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/permissions-settings.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -6
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -4
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/templates/openxiangda-react-spa/AGENTS.md +5 -11
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -8
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
package/lib/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { builtinModules } = require('module');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
6
|
+
const { spawn, spawnSync } = require('child_process');
|
|
7
7
|
const { pathToFileURL } = require('url');
|
|
8
8
|
const esbuild = require('esbuild');
|
|
9
9
|
const {
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
} = require('./config');
|
|
21
21
|
const { requestJson: rawRequestJson } = require('./http');
|
|
22
22
|
const { runWorkspaceJsCodeBuildBatch } = require('./js-code-build');
|
|
23
|
+
const { loadLocalFormSchema } = require('./form-schema-loader');
|
|
23
24
|
const {
|
|
24
25
|
buildGitBaseSourceArtifacts,
|
|
25
26
|
canonicalJsonSha256,
|
|
@@ -63,6 +64,12 @@ const {
|
|
|
63
64
|
inspectReleaseSourceIntegration,
|
|
64
65
|
prepareReleaseSourceRevision,
|
|
65
66
|
} = require('./release-mainline');
|
|
67
|
+
const {
|
|
68
|
+
buildWorkspaceReleaseCommands,
|
|
69
|
+
buildWorkspaceReleaseSteps,
|
|
70
|
+
normalizeReleaseTargets,
|
|
71
|
+
releasePlanHash,
|
|
72
|
+
} = require('./release-plan');
|
|
66
73
|
const {
|
|
67
74
|
analyzeManifestSourceDependencies,
|
|
68
75
|
getImpactedResourceTargets,
|
|
@@ -221,7 +228,7 @@ Usage:
|
|
|
221
228
|
openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
|
|
222
229
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
223
230
|
openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
224
|
-
openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
|
|
231
|
+
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
|
|
225
232
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
226
233
|
openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
|
|
227
234
|
openxiangda env [--profile name]
|
|
@@ -729,9 +736,11 @@ async function sdd(args) {
|
|
|
729
736
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
730
737
|
const result = createMainlineSddBundle({
|
|
731
738
|
cwd: process.cwd(),
|
|
739
|
+
configText: readWorkspaceConfigText(),
|
|
732
740
|
changeId,
|
|
733
741
|
title: flags.title || flags.name,
|
|
734
742
|
changes: flags.changes,
|
|
743
|
+
profile: flags.profile,
|
|
735
744
|
force: Boolean(flags.force),
|
|
736
745
|
});
|
|
737
746
|
if (flags.json) return writeJson(result);
|
|
@@ -884,15 +893,639 @@ function parseSddAffectedFlags(flags) {
|
|
|
884
893
|
};
|
|
885
894
|
}
|
|
886
895
|
|
|
896
|
+
function releaseExecutionDir(changeId) {
|
|
897
|
+
return path.join(
|
|
898
|
+
process.cwd(),
|
|
899
|
+
'.openxiangda',
|
|
900
|
+
'releases',
|
|
901
|
+
String(changeId || '').trim()
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function releaseExecutionFile(changeId) {
|
|
906
|
+
return path.join(releaseExecutionDir(changeId), 'execution.json');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function writePrivateJsonAtomic(file, value) {
|
|
910
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
911
|
+
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
912
|
+
fs.writeFileSync(tempFile, `${JSON.stringify(value, null, 2)}\n`, {
|
|
913
|
+
mode: 0o600,
|
|
914
|
+
});
|
|
915
|
+
fs.renameSync(tempFile, file);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function readReleaseExecution(changeId) {
|
|
919
|
+
const file = releaseExecutionFile(changeId);
|
|
920
|
+
if (!fs.existsSync(file)) return null;
|
|
921
|
+
try {
|
|
922
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
923
|
+
} catch (error) {
|
|
924
|
+
fail(
|
|
925
|
+
`RELEASE_EXECUTION_INVALID: ${path.relative(process.cwd(), file)} 无法读取: ${error.message}`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function summarizeReleaseStepResult(result) {
|
|
931
|
+
const value =
|
|
932
|
+
result && typeof result === 'object' && !Array.isArray(result)
|
|
933
|
+
? result
|
|
934
|
+
: {};
|
|
935
|
+
const findId = candidate =>
|
|
936
|
+
String(
|
|
937
|
+
candidate?.id ||
|
|
938
|
+
candidate?.releaseId ||
|
|
939
|
+
candidate?.activeAppReleaseId ||
|
|
940
|
+
''
|
|
941
|
+
).trim() || null;
|
|
942
|
+
return {
|
|
943
|
+
resultHash: canonicalJsonSha256(value),
|
|
944
|
+
releaseId:
|
|
945
|
+
findId(value.activated) ||
|
|
946
|
+
findId(value.verified) ||
|
|
947
|
+
findId(value.prepared) ||
|
|
948
|
+
findId(value),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
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
|
+
|
|
1006
|
+
function parseChildJsonOutput(stdout) {
|
|
1007
|
+
const text = String(stdout || '').trim();
|
|
1008
|
+
if (!text) return null;
|
|
1009
|
+
try {
|
|
1010
|
+
return JSON.parse(text);
|
|
1011
|
+
} catch {
|
|
1012
|
+
const lines = text.split(/\r?\n/);
|
|
1013
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1014
|
+
const candidate = lines.slice(index).join('\n').trim();
|
|
1015
|
+
if (!candidate.startsWith('{') && !candidate.startsWith('[')) continue;
|
|
1016
|
+
try {
|
|
1017
|
+
return JSON.parse(candidate);
|
|
1018
|
+
} catch {
|
|
1019
|
+
// Continue until the first complete JSON payload is found.
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { output: maskText(text).slice(-4000) };
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function runOpenXiangdaChild(args, options = {}) {
|
|
1027
|
+
return new Promise((resolve, reject) => {
|
|
1028
|
+
const bin = path.join(__dirname, '..', 'bin', 'openxiangda.js');
|
|
1029
|
+
const child = spawn(process.execPath, [bin, ...args, '--json'], {
|
|
1030
|
+
cwd: process.cwd(),
|
|
1031
|
+
env: process.env,
|
|
1032
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1033
|
+
});
|
|
1034
|
+
let stdout = '';
|
|
1035
|
+
let stderr = '';
|
|
1036
|
+
child.stdout.on('data', chunk => {
|
|
1037
|
+
stdout += String(chunk);
|
|
1038
|
+
});
|
|
1039
|
+
child.stderr.on('data', chunk => {
|
|
1040
|
+
const text = String(chunk);
|
|
1041
|
+
stderr += text;
|
|
1042
|
+
if (!options.quiet) warn(maskText(text).trimEnd());
|
|
1043
|
+
});
|
|
1044
|
+
child.on('error', reject);
|
|
1045
|
+
child.on('close', code => {
|
|
1046
|
+
if (code === 0) {
|
|
1047
|
+
resolve(parseChildJsonOutput(stdout));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const message = maskText(
|
|
1051
|
+
String(stderr || stdout || `openxiangda ${args.join(' ')} failed`)
|
|
1052
|
+
).trim();
|
|
1053
|
+
const error = new Error(message);
|
|
1054
|
+
error.code =
|
|
1055
|
+
message.match(/\b([A-Z][A-Z0-9_]{3,})\b/)?.[1] ||
|
|
1056
|
+
'RELEASE_STEP_FAILED';
|
|
1057
|
+
error.exitCode = code;
|
|
1058
|
+
reject(error);
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function normalizeReleaseWaitSeconds(flags) {
|
|
1064
|
+
const raw =
|
|
1065
|
+
readStringFlag(flags, 'wait-seconds') ||
|
|
1066
|
+
readStringFlag(flags, 'lease-wait-seconds');
|
|
1067
|
+
if (raw === '') return 1800;
|
|
1068
|
+
const value = Number(raw);
|
|
1069
|
+
if (!Number.isFinite(value) || value < 0 || value > 86400) {
|
|
1070
|
+
fail('--wait-seconds 必须是 0-86400 之间的秒数');
|
|
1071
|
+
}
|
|
1072
|
+
return Math.floor(value);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
1076
|
+
if (getUsableStoredPublishLease(target)) return { waitedMs: 0 };
|
|
1077
|
+
const waitSeconds = normalizeReleaseWaitSeconds(flags);
|
|
1078
|
+
const startedAt = Date.now();
|
|
1079
|
+
const deadline = startedAt + waitSeconds * 1000;
|
|
1080
|
+
const clientSessionId = createPublishClientSessionId();
|
|
1081
|
+
let attempts = 0;
|
|
1082
|
+
while (true) {
|
|
1083
|
+
const remote = await fetchPublishLeaseStatus(
|
|
1084
|
+
config,
|
|
1085
|
+
target,
|
|
1086
|
+
clientSessionId
|
|
1087
|
+
);
|
|
1088
|
+
if (!remote?.active) {
|
|
1089
|
+
return { waitedMs: Date.now() - startedAt, attempts, remote };
|
|
1090
|
+
}
|
|
1091
|
+
const remoteHolder =
|
|
1092
|
+
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1093
|
+
remote?.holder?.changeId ||
|
|
1094
|
+
remote?.changeId ||
|
|
1095
|
+
remote?.holder?.clientSessionId ||
|
|
1096
|
+
'another release';
|
|
1097
|
+
if (Date.now() >= deadline) {
|
|
1098
|
+
fail(
|
|
1099
|
+
`PUBLISH_LEASE_WAIT_TIMEOUT: ${target.appType} 仍由 ${remoteHolder} 发布中;已等待 ${waitSeconds}s`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
attempts += 1;
|
|
1103
|
+
const suggestedSeconds = Number(
|
|
1104
|
+
remote?.retryAfterSeconds || remote?.retryAfter || 5
|
|
1105
|
+
);
|
|
1106
|
+
const delayMs = Math.min(
|
|
1107
|
+
Math.max(Number.isFinite(suggestedSeconds) ? suggestedSeconds * 1000 : 5000, 1000),
|
|
1108
|
+
30000,
|
|
1109
|
+
Math.max(1000, deadline - Date.now())
|
|
1110
|
+
);
|
|
1111
|
+
warn(
|
|
1112
|
+
`发布队列等待中: app=${target.appType} holder=${remoteHolder} retry=${Math.ceil(delayMs / 1000)}s`
|
|
1113
|
+
);
|
|
1114
|
+
await sleep(delayMs);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
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
|
+
|
|
1250
|
+
async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
1251
|
+
const changeId = readStringFlag(flags, 'change');
|
|
1252
|
+
if (!changeId) {
|
|
1253
|
+
fail('用法: openxiangda release publish --change <id> --profile <name>');
|
|
1254
|
+
}
|
|
1255
|
+
const plan = buildWorkspaceReleasePlan({
|
|
1256
|
+
changed: true,
|
|
1257
|
+
since: flags.since,
|
|
1258
|
+
profile: target.profileName,
|
|
1259
|
+
skipSdd: true,
|
|
1260
|
+
changeId,
|
|
1261
|
+
expandDependencies: Boolean(flags['expand-dependencies']),
|
|
1262
|
+
});
|
|
1263
|
+
if (plan.errors.length > 0) {
|
|
1264
|
+
fail(
|
|
1265
|
+
`RELEASE_PLAN_INVALID: ${plan.errors
|
|
1266
|
+
.map(error => error.message || error.name)
|
|
1267
|
+
.join('; ')}`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const steps = buildWorkspaceReleaseSteps(
|
|
1271
|
+
plan.targets,
|
|
1272
|
+
plan.workspace.runtimeMode,
|
|
1273
|
+
target.profileName,
|
|
1274
|
+
changeId
|
|
1275
|
+
);
|
|
1276
|
+
const planHash = releasePlanHash({
|
|
1277
|
+
schemaVersion: 'openxiangda_release_execution_v1',
|
|
1278
|
+
appType: target.appType,
|
|
1279
|
+
profile: target.profileName,
|
|
1280
|
+
changeId,
|
|
1281
|
+
runtimeMode: plan.workspace.runtimeMode,
|
|
1282
|
+
targets: plan.activationTargets,
|
|
1283
|
+
commands: steps.map(step => step.command),
|
|
1284
|
+
});
|
|
1285
|
+
if (flags['dry-run']) {
|
|
1286
|
+
return {
|
|
1287
|
+
dryRun: true,
|
|
1288
|
+
appType: target.appType,
|
|
1289
|
+
profile: target.profileName,
|
|
1290
|
+
changeId,
|
|
1291
|
+
planHash,
|
|
1292
|
+
targets: plan.targets,
|
|
1293
|
+
activationTargets: plan.activationTargets,
|
|
1294
|
+
steps,
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const verification = verifySddChange({
|
|
1298
|
+
cwd: process.cwd(),
|
|
1299
|
+
changeId,
|
|
1300
|
+
releasePlan: {
|
|
1301
|
+
...plan,
|
|
1302
|
+
commands: steps.map(step => step.command),
|
|
1303
|
+
},
|
|
1304
|
+
writeMetadata: true,
|
|
1305
|
+
stage: 'prepublish',
|
|
1306
|
+
});
|
|
1307
|
+
if (!verification.passed) {
|
|
1308
|
+
fail(
|
|
1309
|
+
`SDD_PREPUBLISH_FAILED: ${verification.errors
|
|
1310
|
+
.map(error => error.message)
|
|
1311
|
+
.join('; ')}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
if (steps.length === 0) {
|
|
1315
|
+
return {
|
|
1316
|
+
completed: true,
|
|
1317
|
+
noOp: true,
|
|
1318
|
+
appType: target.appType,
|
|
1319
|
+
profile: target.profileName,
|
|
1320
|
+
changeId,
|
|
1321
|
+
planHash,
|
|
1322
|
+
execution: null,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const existing = readReleaseExecution(changeId);
|
|
1326
|
+
if (
|
|
1327
|
+
existing &&
|
|
1328
|
+
(existing.planHash !== planHash ||
|
|
1329
|
+
existing.appType !== target.appType ||
|
|
1330
|
+
existing.profile !== target.profileName)
|
|
1331
|
+
) {
|
|
1332
|
+
fail(
|
|
1333
|
+
'RELEASE_EXECUTION_PLAN_CHANGED: 已存在不同计划的执行日志;请先完成或人工核对旧发布,禁止静默覆盖'
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (
|
|
1337
|
+
existing?.status === 'write-review-required' &&
|
|
1338
|
+
!flags['resume-after-review']
|
|
1339
|
+
) {
|
|
1340
|
+
fail(
|
|
1341
|
+
'RELEASE_WRITE_REVIEW_REQUIRED: 上次写请求结果不确定;请先只读核对 staged release,确认可重试后追加 --resume-after-review'
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const now = new Date().toISOString();
|
|
1345
|
+
const execution = existing || {
|
|
1346
|
+
schemaVersion: 'openxiangda_release_execution_v1',
|
|
1347
|
+
appType: target.appType,
|
|
1348
|
+
profile: target.profileName,
|
|
1349
|
+
changeId,
|
|
1350
|
+
planHash,
|
|
1351
|
+
status: 'planned',
|
|
1352
|
+
createdAt: now,
|
|
1353
|
+
updatedAt: now,
|
|
1354
|
+
stagedWriteOccurred: false,
|
|
1355
|
+
writeAttempted: false,
|
|
1356
|
+
steps: [
|
|
1357
|
+
{
|
|
1358
|
+
id: 'lease-and-capture',
|
|
1359
|
+
command: `openxiangda release begin --change ${changeId} --profile ${target.profileName} --freeze-app-capture`,
|
|
1360
|
+
status: 'pending',
|
|
1361
|
+
},
|
|
1362
|
+
...steps.map(step => ({
|
|
1363
|
+
id: step.id,
|
|
1364
|
+
command: step.command,
|
|
1365
|
+
stagedKind: step.stagedKind,
|
|
1366
|
+
status: 'pending',
|
|
1367
|
+
})),
|
|
1368
|
+
{
|
|
1369
|
+
id: 'release-lease',
|
|
1370
|
+
command: `openxiangda release end --change ${changeId} --profile ${target.profileName}`,
|
|
1371
|
+
status: 'pending',
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
};
|
|
1375
|
+
if (execution.status === 'completed') {
|
|
1376
|
+
return {
|
|
1377
|
+
completed: true,
|
|
1378
|
+
resumed: true,
|
|
1379
|
+
appType: target.appType,
|
|
1380
|
+
profile: target.profileName,
|
|
1381
|
+
changeId,
|
|
1382
|
+
planHash,
|
|
1383
|
+
executionFile: path
|
|
1384
|
+
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1385
|
+
.replace(/\\/g, '/'),
|
|
1386
|
+
execution,
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
execution.status = 'running';
|
|
1390
|
+
execution.updatedAt = new Date().toISOString();
|
|
1391
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1392
|
+
|
|
1393
|
+
const runnable = [
|
|
1394
|
+
{
|
|
1395
|
+
id: 'lease-and-capture',
|
|
1396
|
+
args: [
|
|
1397
|
+
'release',
|
|
1398
|
+
'begin',
|
|
1399
|
+
'--change',
|
|
1400
|
+
changeId,
|
|
1401
|
+
'--profile',
|
|
1402
|
+
target.profileName,
|
|
1403
|
+
'--freeze-app-capture',
|
|
1404
|
+
],
|
|
1405
|
+
},
|
|
1406
|
+
...steps,
|
|
1407
|
+
{
|
|
1408
|
+
id: 'release-lease',
|
|
1409
|
+
args: [
|
|
1410
|
+
'release',
|
|
1411
|
+
'end',
|
|
1412
|
+
'--change',
|
|
1413
|
+
changeId,
|
|
1414
|
+
'--profile',
|
|
1415
|
+
target.profileName,
|
|
1416
|
+
],
|
|
1417
|
+
},
|
|
1418
|
+
];
|
|
1419
|
+
try {
|
|
1420
|
+
for (const step of runnable) {
|
|
1421
|
+
const journalStep = execution.steps.find(item => item.id === step.id);
|
|
1422
|
+
if (journalStep?.status === 'completed') continue;
|
|
1423
|
+
if (step.id === 'lease-and-capture') {
|
|
1424
|
+
await waitForPublishLeaseAvailability(config, target, flags);
|
|
1425
|
+
}
|
|
1426
|
+
journalStep.status = 'running';
|
|
1427
|
+
journalStep.startedAt = new Date().toISOString();
|
|
1428
|
+
execution.updatedAt = journalStep.startedAt;
|
|
1429
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1430
|
+
warn(`发布步骤开始: ${step.id}`);
|
|
1431
|
+
const result = await runOpenXiangdaChild(step.args);
|
|
1432
|
+
journalStep.status = 'completed';
|
|
1433
|
+
journalStep.completedAt = new Date().toISOString();
|
|
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
|
+
}
|
|
1449
|
+
if (step.stagedKind) execution.stagedWriteOccurred = true;
|
|
1450
|
+
execution.updatedAt = journalStep.completedAt;
|
|
1451
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1452
|
+
}
|
|
1453
|
+
execution.status = 'completed';
|
|
1454
|
+
execution.completedAt = new Date().toISOString();
|
|
1455
|
+
execution.updatedAt = execution.completedAt;
|
|
1456
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1457
|
+
return {
|
|
1458
|
+
completed: true,
|
|
1459
|
+
resumed: Boolean(existing),
|
|
1460
|
+
appType: target.appType,
|
|
1461
|
+
profile: target.profileName,
|
|
1462
|
+
changeId,
|
|
1463
|
+
planHash,
|
|
1464
|
+
executionFile: path
|
|
1465
|
+
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1466
|
+
.replace(/\\/g, '/'),
|
|
1467
|
+
execution,
|
|
1468
|
+
};
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
const runningStep = execution.steps.find(step => step.status === 'running');
|
|
1471
|
+
if (runningStep) {
|
|
1472
|
+
runningStep.status = 'failed';
|
|
1473
|
+
runningStep.failedAt = new Date().toISOString();
|
|
1474
|
+
runningStep.error = {
|
|
1475
|
+
code: error.code || 'RELEASE_STEP_FAILED',
|
|
1476
|
+
message: maskText(error.message).slice(0, 1000),
|
|
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
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
execution.status = execution.stagedWriteOccurred
|
|
1492
|
+
? 'staged-resumable'
|
|
1493
|
+
: execution.writeAttempted
|
|
1494
|
+
? 'write-review-required'
|
|
1495
|
+
: 'failed';
|
|
1496
|
+
execution.updatedAt = new Date().toISOString();
|
|
1497
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1498
|
+
if (!execution.writeAttempted) {
|
|
1499
|
+
try {
|
|
1500
|
+
await runOpenXiangdaChild(
|
|
1501
|
+
[
|
|
1502
|
+
'release',
|
|
1503
|
+
'end',
|
|
1504
|
+
'--change',
|
|
1505
|
+
changeId,
|
|
1506
|
+
'--profile',
|
|
1507
|
+
target.profileName,
|
|
1508
|
+
],
|
|
1509
|
+
{ quiet: true }
|
|
1510
|
+
);
|
|
1511
|
+
} catch {
|
|
1512
|
+
// The original error remains authoritative; lease cleanup is best effort.
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
887
1519
|
async function release(args) {
|
|
888
1520
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
889
1521
|
const { flags, positional } = parseArgs(rest);
|
|
890
1522
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
891
1523
|
print([
|
|
892
|
-
'用法: openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
1524
|
+
'用法: openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
893
1525
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
894
1526
|
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
|
|
895
1527
|
'常用流程:',
|
|
1528
|
+
' openxiangda release publish --change <id> --profile <name>',
|
|
896
1529
|
' openxiangda release begin --change <id> --profile <name>',
|
|
897
1530
|
' openxiangda resource publish function --only <code> --change <id> --profile <name>',
|
|
898
1531
|
' openxiangda runtime deploy --change <id> --profile <name>',
|
|
@@ -903,6 +1536,8 @@ async function release(args) {
|
|
|
903
1536
|
' openxiangda release backend-rollback <releaseId> --change <id> --reason "..." --profile <name>',
|
|
904
1537
|
' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
|
|
905
1538
|
'说明:',
|
|
1539
|
+
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
1540
|
+
' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
|
|
906
1541
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
907
1542
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
908
1543
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
@@ -941,6 +1576,16 @@ async function release(args) {
|
|
|
941
1576
|
flags
|
|
942
1577
|
);
|
|
943
1578
|
}
|
|
1579
|
+
if (subcommand === 'publish') {
|
|
1580
|
+
const result = await publishWorkspaceRelease(config, target, flags);
|
|
1581
|
+
if (flags.json) return writeJson(result);
|
|
1582
|
+
print(
|
|
1583
|
+
result.dryRun
|
|
1584
|
+
? `发布计划已生成(未写入): ${result.changeId} steps=${result.steps.length}`
|
|
1585
|
+
: `应用原子发布完成: ${result.appType} change=${result.changeId}`
|
|
1586
|
+
);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
944
1589
|
const leaseIdFromArgs = positional[0] || readStringFlag(flags, 'lease-id');
|
|
945
1590
|
|
|
946
1591
|
if (subcommand === 'status') {
|
|
@@ -1040,15 +1685,56 @@ async function release(args) {
|
|
|
1040
1685
|
throw error;
|
|
1041
1686
|
}
|
|
1042
1687
|
const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1688
|
+
let frozenCapture = null;
|
|
1689
|
+
let releaseSessionFile = null;
|
|
1690
|
+
let stagedResourcesFile = null;
|
|
1691
|
+
try {
|
|
1692
|
+
if (flags['freeze-app-capture']) {
|
|
1693
|
+
frozenCapture = await fetchAppReleaseCapture(config, target, flags);
|
|
1694
|
+
releaseSessionFile = saveFrozenAppReleaseCapture(
|
|
1695
|
+
target,
|
|
1696
|
+
changeId,
|
|
1697
|
+
finalizedBaseline,
|
|
1698
|
+
lease,
|
|
1699
|
+
frozenCapture
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
stagedResourcesFile = newReleaseLifecycle
|
|
1703
|
+
? initializeStagedAppReleaseResources(target, changeId)
|
|
1704
|
+
: null;
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
if (newReleaseLifecycle) {
|
|
1707
|
+
try {
|
|
1708
|
+
await requestWithAuth(
|
|
1709
|
+
config,
|
|
1710
|
+
target.profileName,
|
|
1711
|
+
publishLeaseApiPath(
|
|
1712
|
+
target,
|
|
1713
|
+
`/${encodeURIComponent(lease.leaseId)}/release`
|
|
1714
|
+
),
|
|
1715
|
+
{ method: 'POST', body: { completion: 'local-prepare-failed' } }
|
|
1716
|
+
);
|
|
1717
|
+
} catch {
|
|
1718
|
+
// Preserve the local preparation failure as the primary error.
|
|
1719
|
+
}
|
|
1720
|
+
clearPublishLease(target, lease.leaseId);
|
|
1721
|
+
clearChangeBaseline(target, finalizedBaseline?.baselineId);
|
|
1722
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1723
|
+
}
|
|
1724
|
+
throw error;
|
|
1725
|
+
}
|
|
1046
1726
|
const result = {
|
|
1047
1727
|
...lease,
|
|
1048
1728
|
baselineId: finalizedBaseline?.baselineId,
|
|
1049
1729
|
baselineHeadDigest: finalizedBaseline?.headDigest,
|
|
1050
1730
|
sourceBase: finalizedBaseline?.sourceBase,
|
|
1051
1731
|
releaseSourceRevision: finalizedBaseline?.releaseSourceRevision,
|
|
1732
|
+
...(frozenCapture
|
|
1733
|
+
? {
|
|
1734
|
+
captureHash: frozenCapture.captureHash,
|
|
1735
|
+
releaseSessionFile,
|
|
1736
|
+
}
|
|
1737
|
+
: {}),
|
|
1052
1738
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
1053
1739
|
};
|
|
1054
1740
|
if (flags.json) return writeJson(result);
|
|
@@ -1119,34 +1805,22 @@ async function release(args) {
|
|
|
1119
1805
|
return;
|
|
1120
1806
|
}
|
|
1121
1807
|
if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
|
|
1122
|
-
assertOrClaimWorktreeOwner({
|
|
1123
|
-
cwd: process.cwd(),
|
|
1124
|
-
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1125
|
-
});
|
|
1126
1808
|
const leaseId = leaseIdFromArgs || stored?.leaseId;
|
|
1127
1809
|
if (!leaseId) {
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
fail(
|
|
1135
|
-
'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
const result = {
|
|
1139
|
-
active: false,
|
|
1140
|
-
appType: target.appType,
|
|
1141
|
-
local: false,
|
|
1142
|
-
integration,
|
|
1143
|
-
};
|
|
1144
|
-
if (baseline) clearChangeBaseline(target, baseline.baselineId);
|
|
1145
|
-
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1810
|
+
const result = await endReleaseWithoutLocalLease(
|
|
1811
|
+
config,
|
|
1812
|
+
target,
|
|
1813
|
+
flags,
|
|
1814
|
+
baseline
|
|
1815
|
+
);
|
|
1146
1816
|
if (flags.json) return writeJson(result);
|
|
1147
1817
|
print(`当前工作区没有活动发布租约: ${target.appType}`);
|
|
1148
1818
|
return;
|
|
1149
1819
|
}
|
|
1820
|
+
assertOrClaimWorktreeOwner({
|
|
1821
|
+
cwd: process.cwd(),
|
|
1822
|
+
changeId: stored?.changeId || readStringFlag(flags, 'change') || 'unscoped-publish',
|
|
1823
|
+
});
|
|
1150
1824
|
const integration = assertReleaseSourceIntegrated(
|
|
1151
1825
|
baseline?.releaseSourceRevision,
|
|
1152
1826
|
{ cwd: process.cwd() }
|
|
@@ -1191,7 +1865,7 @@ async function release(args) {
|
|
|
1191
1865
|
}
|
|
1192
1866
|
}
|
|
1193
1867
|
|
|
1194
|
-
fail('用法: openxiangda release begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
|
|
1868
|
+
fail('用法: openxiangda release publish|begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
|
|
1195
1869
|
}
|
|
1196
1870
|
|
|
1197
1871
|
async function fetchPublishLeaseStatus(config, target, clientSessionId) {
|
|
@@ -1460,6 +2134,74 @@ async function fetchAppReleaseCapture(config, target, flags = {}) {
|
|
|
1460
2134
|
return clonePlainJson(capture);
|
|
1461
2135
|
}
|
|
1462
2136
|
|
|
2137
|
+
function releaseSessionFile(changeId) {
|
|
2138
|
+
return path.join(releaseExecutionDir(changeId), 'release-session.json');
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function saveFrozenAppReleaseCapture(
|
|
2142
|
+
target,
|
|
2143
|
+
changeId,
|
|
2144
|
+
baseline,
|
|
2145
|
+
lease,
|
|
2146
|
+
capture
|
|
2147
|
+
) {
|
|
2148
|
+
const file = releaseSessionFile(changeId);
|
|
2149
|
+
writePrivateJsonAtomic(file, {
|
|
2150
|
+
schemaVersion: 'openxiangda_release_session_v1',
|
|
2151
|
+
appType: target.appType,
|
|
2152
|
+
profile: target.profileName,
|
|
2153
|
+
changeId,
|
|
2154
|
+
baselineId: baseline?.baselineId || baseline?.id || null,
|
|
2155
|
+
leaseId: lease?.leaseId || lease?.id || null,
|
|
2156
|
+
sourceRevision: baseline?.releaseSourceRevision || null,
|
|
2157
|
+
capturedAt: new Date().toISOString(),
|
|
2158
|
+
capture,
|
|
2159
|
+
});
|
|
2160
|
+
return path.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function readFrozenAppReleaseCapture(target, flags = {}) {
|
|
2164
|
+
const changeId = readStringFlag(flags, 'change');
|
|
2165
|
+
if (!changeId) return null;
|
|
2166
|
+
const file = releaseSessionFile(changeId);
|
|
2167
|
+
if (!fs.existsSync(file)) return null;
|
|
2168
|
+
let session;
|
|
2169
|
+
try {
|
|
2170
|
+
session = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
fail(`RELEASE_SESSION_INVALID: ${error.message}`);
|
|
2173
|
+
}
|
|
2174
|
+
if (
|
|
2175
|
+
session?.schemaVersion !== 'openxiangda_release_session_v1' ||
|
|
2176
|
+
session?.appType !== target.appType ||
|
|
2177
|
+
session?.profile !== target.profileName ||
|
|
2178
|
+
session?.changeId !== changeId
|
|
2179
|
+
) {
|
|
2180
|
+
fail(
|
|
2181
|
+
'RELEASE_SESSION_CONTEXT_MISMATCH: 冻结的 App capture 与当前 app/profile/change 不一致'
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
const capture = session.capture;
|
|
2185
|
+
if (
|
|
2186
|
+
!capture ||
|
|
2187
|
+
!Array.isArray(capture.resources) ||
|
|
2188
|
+
!/^[a-f0-9]{64}$/i.test(String(capture.captureHash || ''))
|
|
2189
|
+
) {
|
|
2190
|
+
fail('RELEASE_SESSION_CAPTURE_INVALID: 冻结的 App capture 不完整');
|
|
2191
|
+
}
|
|
2192
|
+
const expectedCaptureHash = readStringFlag(flags, 'expected-capture-hash');
|
|
2193
|
+
if (
|
|
2194
|
+
expectedCaptureHash &&
|
|
2195
|
+
expectedCaptureHash.toLowerCase() !==
|
|
2196
|
+
String(capture.captureHash).toLowerCase()
|
|
2197
|
+
) {
|
|
2198
|
+
fail(
|
|
2199
|
+
`APP_RELEASE_CAPTURE_MOVED: 期望 capture ${expectedCaptureHash},冻结为 ${capture.captureHash}`
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
return clonePlainJson(capture);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
1463
2205
|
function appReleaseWriteBody(releaseContext, extra = {}) {
|
|
1464
2206
|
return {
|
|
1465
2207
|
...releaseContext.control,
|
|
@@ -1790,7 +2532,13 @@ function assertStagedAppReleaseScope(target, flags, resources) {
|
|
|
1790
2532
|
}
|
|
1791
2533
|
throw error;
|
|
1792
2534
|
}
|
|
1793
|
-
const
|
|
2535
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(
|
|
2536
|
+
readWorkspaceConfigText()
|
|
2537
|
+
);
|
|
2538
|
+
const { activationTargets: targets } = normalizeReleaseTargets(
|
|
2539
|
+
scope.targets || {},
|
|
2540
|
+
runtimeMode
|
|
2541
|
+
);
|
|
1794
2542
|
const normalized = normalizeStagedAppReleaseResources(resources);
|
|
1795
2543
|
const byKind = kind => normalized.filter(resource => resource.kind === kind);
|
|
1796
2544
|
const expectedBackendSelectors = [
|
|
@@ -1873,8 +2621,15 @@ function changedAppReleaseResources(detailResources, captureResources) {
|
|
|
1873
2621
|
}
|
|
1874
2622
|
|
|
1875
2623
|
function assertAppReleaseActualGate(target, flags, scope, command) {
|
|
2624
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(
|
|
2625
|
+
readWorkspaceConfigText()
|
|
2626
|
+
);
|
|
2627
|
+
const normalized = normalizeReleaseTargets(
|
|
2628
|
+
scope.targets || {},
|
|
2629
|
+
runtimeMode
|
|
2630
|
+
);
|
|
1876
2631
|
assertSddReleaseGate(
|
|
1877
|
-
buildExplicitSddReleasePlan(
|
|
2632
|
+
buildExplicitSddReleasePlan(normalized.activationTargets, {
|
|
1878
2633
|
files: scope.changedFiles || [],
|
|
1879
2634
|
commands: [command],
|
|
1880
2635
|
changeId: scope.changeId,
|
|
@@ -2042,6 +2797,70 @@ async function activateAppReleaseDetail(
|
|
|
2042
2797
|
);
|
|
2043
2798
|
}
|
|
2044
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
|
+
|
|
2045
2864
|
async function runAppReleaseCommand(
|
|
2046
2865
|
config,
|
|
2047
2866
|
target,
|
|
@@ -2059,6 +2878,7 @@ async function runAppReleaseCommand(
|
|
|
2059
2878
|
target.profileName,
|
|
2060
2879
|
appReleaseApiPath(target, '/head')
|
|
2061
2880
|
);
|
|
2881
|
+
if (!flags.full) data = summarizeAppReleaseHead(data);
|
|
2062
2882
|
} else if (subcommand === 'app-list') {
|
|
2063
2883
|
data = await requestWithAuth(
|
|
2064
2884
|
config,
|
|
@@ -2236,7 +3056,9 @@ async function runAppReleaseCommand(
|
|
|
2236
3056
|
flags,
|
|
2237
3057
|
'App Release finalize'
|
|
2238
3058
|
);
|
|
2239
|
-
const capture =
|
|
3059
|
+
const capture =
|
|
3060
|
+
readFrozenAppReleaseCapture(target, flags) ||
|
|
3061
|
+
(await fetchAppReleaseCapture(config, target, flags));
|
|
2240
3062
|
const frozenResources = overlayAppReleaseResources(
|
|
2241
3063
|
capture.resources,
|
|
2242
3064
|
stagedResources
|
|
@@ -8761,6 +9583,9 @@ async function resource(args) {
|
|
|
8761
9583
|
fail('resource publish --only/--code 不能与 --prune 同时使用,避免删除未选中的资源');
|
|
8762
9584
|
}
|
|
8763
9585
|
const manifest = loadWorkspaceResources({ typeFilters, codeFilters });
|
|
9586
|
+
if (subcommand !== 'typegen') {
|
|
9587
|
+
await attachLocalFormSchemas(manifest);
|
|
9588
|
+
}
|
|
8764
9589
|
if (
|
|
8765
9590
|
subcommand === 'plan' &&
|
|
8766
9591
|
process.env.OPENXIANGDA_GIT_BASE_RESOURCE_PLAN === '1'
|
|
@@ -9560,12 +10385,23 @@ async function runtime(args) {
|
|
|
9560
10385
|
}
|
|
9561
10386
|
|
|
9562
10387
|
async function loadRuntimeHeadApp(config, target) {
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
10388
|
+
try {
|
|
10389
|
+
return (
|
|
10390
|
+
(await requestWithAuth(
|
|
10391
|
+
config,
|
|
10392
|
+
target.profileName,
|
|
10393
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases/head`
|
|
10394
|
+
)) || {}
|
|
10395
|
+
);
|
|
10396
|
+
} catch (error) {
|
|
10397
|
+
if (!isHttpNotFound(error)) throw error;
|
|
10398
|
+
const snapshot = await requestWithAuth(
|
|
10399
|
+
config,
|
|
10400
|
+
target.profileName,
|
|
10401
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/snapshot`
|
|
10402
|
+
);
|
|
10403
|
+
return snapshot?.app || snapshot?.application || snapshot || {};
|
|
10404
|
+
}
|
|
9569
10405
|
}
|
|
9570
10406
|
|
|
9571
10407
|
function assertRuntimeSourceRevisionStable(beforeBuild, afterBuild) {
|
|
@@ -10180,7 +11016,7 @@ async function commands(args) {
|
|
|
10180
11016
|
'open-api credential list|get|create|update|rotate-secret [id]',
|
|
10181
11017
|
'design gates|template|review [--topic code]',
|
|
10182
11018
|
'sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive',
|
|
10183
|
-
'release begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
|
|
11019
|
+
'release publish|begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
|
|
10184
11020
|
'env',
|
|
10185
11021
|
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
10186
11022
|
'app list|create|snapshot',
|
|
@@ -11646,6 +12482,84 @@ function loadWorkspaceResources(options = {}) {
|
|
|
11646
12482
|
return manifest;
|
|
11647
12483
|
}
|
|
11648
12484
|
|
|
12485
|
+
async function attachLocalFormSchemas(manifest) {
|
|
12486
|
+
if (
|
|
12487
|
+
Array.isArray(manifest.resourceTypeFilters) &&
|
|
12488
|
+
!manifest.resourceTypeFilters.includes('formSettings')
|
|
12489
|
+
) {
|
|
12490
|
+
return manifest;
|
|
12491
|
+
}
|
|
12492
|
+
const selectors = Array.isArray(manifest.__resourceCodeSelectors)
|
|
12493
|
+
? manifest.__resourceCodeSelectors.filter(selector =>
|
|
12494
|
+
resourceCodeSelectorAppliesToKey(selector, 'formSettings')
|
|
12495
|
+
)
|
|
12496
|
+
: [];
|
|
12497
|
+
const candidateCodes = unique([
|
|
12498
|
+
...(manifest.formSettings || []).map(getResourceItemCode),
|
|
12499
|
+
...selectors.map(selector => selector.code),
|
|
12500
|
+
]).filter(Boolean);
|
|
12501
|
+
if (candidateCodes.length === 0) return manifest;
|
|
12502
|
+
|
|
12503
|
+
const loadedCodes = new Set();
|
|
12504
|
+
for (const code of candidateCodes) {
|
|
12505
|
+
let localSchema;
|
|
12506
|
+
try {
|
|
12507
|
+
localSchema = await loadLocalFormSchema(process.cwd(), code);
|
|
12508
|
+
} catch (error) {
|
|
12509
|
+
const wrapped = new Error(
|
|
12510
|
+
`FORM_SCHEMA_LOAD_FAILED: ${code}: ${error.message}`
|
|
12511
|
+
);
|
|
12512
|
+
wrapped.code = 'FORM_SCHEMA_LOAD_FAILED';
|
|
12513
|
+
throw wrapped;
|
|
12514
|
+
}
|
|
12515
|
+
if (!localSchema) continue;
|
|
12516
|
+
loadedCodes.add(code);
|
|
12517
|
+
const existing = (manifest.formSettings || []).find(
|
|
12518
|
+
item => getResourceItemCode(item) === code
|
|
12519
|
+
);
|
|
12520
|
+
const merged = {
|
|
12521
|
+
...(existing || {}),
|
|
12522
|
+
code,
|
|
12523
|
+
formCode: existing?.formCode || code,
|
|
12524
|
+
name: existing?.name ?? localSchema.name,
|
|
12525
|
+
schema: localSchema.schema,
|
|
12526
|
+
packages: localSchema.packages,
|
|
12527
|
+
formType: existing?.formType ?? localSchema.formType,
|
|
12528
|
+
__source: existing?.__source || path.join(
|
|
12529
|
+
process.cwd(),
|
|
12530
|
+
localSchema.sourcePath
|
|
12531
|
+
),
|
|
12532
|
+
__dir: existing?.__dir || path.dirname(
|
|
12533
|
+
path.join(process.cwd(), localSchema.sourcePath)
|
|
12534
|
+
),
|
|
12535
|
+
__localSchema: {
|
|
12536
|
+
sourcePath: localSchema.sourcePath,
|
|
12537
|
+
fieldCount: localSchema.fieldCount,
|
|
12538
|
+
},
|
|
12539
|
+
};
|
|
12540
|
+
if (existing) {
|
|
12541
|
+
Object.assign(existing, merged);
|
|
12542
|
+
} else {
|
|
12543
|
+
manifest.formSettings = manifest.formSettings || [];
|
|
12544
|
+
manifest.formSettings.push(merged);
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
manifest.formSettings = (manifest.formSettings || []).sort((left, right) =>
|
|
12548
|
+
getResourceItemCode(left).localeCompare(getResourceItemCode(right))
|
|
12549
|
+
);
|
|
12550
|
+
if (
|
|
12551
|
+
loadedCodes.size > 0 &&
|
|
12552
|
+
Array.isArray(manifest.unmatchedResourceCodeFilters)
|
|
12553
|
+
) {
|
|
12554
|
+
manifest.unmatchedResourceCodeFilters =
|
|
12555
|
+
manifest.unmatchedResourceCodeFilters.filter(raw => {
|
|
12556
|
+
const code = String(raw || '').replace(/^.*[:/]/, '');
|
|
12557
|
+
return !loadedCodes.has(code);
|
|
12558
|
+
});
|
|
12559
|
+
}
|
|
12560
|
+
return manifest;
|
|
12561
|
+
}
|
|
12562
|
+
|
|
11649
12563
|
function readResourceItems(baseDir, spec, options = {}) {
|
|
11650
12564
|
const items = [];
|
|
11651
12565
|
const selectorEntries = options.selectorEntries || null;
|
|
@@ -20593,6 +21507,16 @@ function getWorkspaceImpactedResourceTargets(files) {
|
|
|
20593
21507
|
return getImpactedResourceTargets(getWorkspaceSourceDependencyIndex(), files);
|
|
20594
21508
|
}
|
|
20595
21509
|
|
|
21510
|
+
function scopeNeedsBackendDependencyAnalysis(scope) {
|
|
21511
|
+
const targets = scope?.targets || {};
|
|
21512
|
+
return (
|
|
21513
|
+
Boolean(targets.resources) ||
|
|
21514
|
+
['functions', 'automations', 'workflows', 'jsCodeNodes'].some(
|
|
21515
|
+
key => (targets[key] || []).length > 0
|
|
21516
|
+
)
|
|
21517
|
+
);
|
|
21518
|
+
}
|
|
21519
|
+
|
|
20596
21520
|
function addCodeResourceTargetFromPath(targets, file, prefix, key, options = {}) {
|
|
20597
21521
|
if (!file.startsWith(prefix)) return false;
|
|
20598
21522
|
const rest = file.slice(prefix.length);
|
|
@@ -20890,8 +21814,11 @@ function fileBelongsToChangeScope(file, scope, runtimeMode) {
|
|
|
20890
21814
|
|
|
20891
21815
|
function getChangeScopedWorkspaceFiles(changeScope, runtimeMode, options = {}) {
|
|
20892
21816
|
const dirtyFiles = getWorkspaceChangedFiles({ since: options.since });
|
|
20893
|
-
const dependencyIndex =
|
|
21817
|
+
const dependencyIndex = scopeNeedsBackendDependencyAnalysis(changeScope)
|
|
21818
|
+
? getWorkspaceSourceDependencyIndex()
|
|
21819
|
+
: null;
|
|
20894
21820
|
const dependencyBelongsToScope = file => {
|
|
21821
|
+
if (!dependencyIndex) return false;
|
|
20895
21822
|
const impacted = getImpactedResourceTargets(dependencyIndex, [file]);
|
|
20896
21823
|
return ['functions', 'automations', 'workflows', 'jsCodeNodes'].some(key =>
|
|
20897
21824
|
(impacted[key] || []).some(code => scopeIncludesCode(changeScope.targets?.[key], code))
|
|
@@ -20986,87 +21913,6 @@ function scanWorkspaceReliabilityIssues(files) {
|
|
|
20986
21913
|
return issues;
|
|
20987
21914
|
}
|
|
20988
21915
|
|
|
20989
|
-
function buildTargetedResourceReleaseCommands(targets, profileArg, changeId) {
|
|
20990
|
-
const changeArg = changeId ? ` --change ${changeId}` : '';
|
|
20991
|
-
const commands = [];
|
|
20992
|
-
const functions = targets.functions || [];
|
|
20993
|
-
const automations = targets.automations || [];
|
|
20994
|
-
if (functions.length > 0 || automations.length > 0) {
|
|
20995
|
-
const resourceTypes = [
|
|
20996
|
-
...(functions.length > 0 ? ['function'] : []),
|
|
20997
|
-
...(automations.length > 0 ? ['automation'] : []),
|
|
20998
|
-
];
|
|
20999
|
-
const qualify = resourceTypes.length > 1;
|
|
21000
|
-
const selectors = [
|
|
21001
|
-
...functions.map(code => (qualify ? `function:${code}` : code)),
|
|
21002
|
-
...automations.map(code => (qualify ? `automation:${code}` : code)),
|
|
21003
|
-
];
|
|
21004
|
-
commands.push(
|
|
21005
|
-
`openxiangda resource publish ${resourceTypes.join(',')} --only ${selectors.join(',')} --stage-only --profile ${profileArg}${changeArg}`
|
|
21006
|
-
);
|
|
21007
|
-
}
|
|
21008
|
-
const workflows = targets.workflows || [];
|
|
21009
|
-
if (workflows.length > 0) {
|
|
21010
|
-
commands.push(
|
|
21011
|
-
`openxiangda resource publish workflow --only ${workflows.join(',')} --profile ${profileArg}${changeArg}`
|
|
21012
|
-
);
|
|
21013
|
-
}
|
|
21014
|
-
return commands;
|
|
21015
|
-
}
|
|
21016
|
-
|
|
21017
|
-
function buildWorkspaceReleaseCommands(targets, runtimeMode, profile, changeId) {
|
|
21018
|
-
const profileArg = profile || '<profile>';
|
|
21019
|
-
const commands = [];
|
|
21020
|
-
const hasResourceTargets =
|
|
21021
|
-
Boolean(targets.resources) ||
|
|
21022
|
-
(targets.functions || []).length > 0 ||
|
|
21023
|
-
(targets.automations || []).length > 0 ||
|
|
21024
|
-
(targets.workflows || []).length > 0 ||
|
|
21025
|
-
(targets.jsCodeNodes || []).length > 0;
|
|
21026
|
-
if (runtimeMode === 'react-spa') {
|
|
21027
|
-
if (targets.forms.length > 0) {
|
|
21028
|
-
commands.push(
|
|
21029
|
-
`openxiangda resource publish form-setting --only ${targets.forms.join(',')} --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
|
|
21030
|
-
);
|
|
21031
|
-
}
|
|
21032
|
-
if (hasResourceTargets) {
|
|
21033
|
-
commands.push(...buildTargetedResourceReleaseCommands(targets, profileArg, changeId));
|
|
21034
|
-
}
|
|
21035
|
-
if (targets.runtime) {
|
|
21036
|
-
commands.push(
|
|
21037
|
-
`openxiangda runtime deploy --no-activate --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
|
|
21038
|
-
);
|
|
21039
|
-
}
|
|
21040
|
-
if (
|
|
21041
|
-
targets.forms.length > 0 ||
|
|
21042
|
-
(targets.functions || []).length > 0 ||
|
|
21043
|
-
(targets.automations || []).length > 0 ||
|
|
21044
|
-
targets.runtime
|
|
21045
|
-
) {
|
|
21046
|
-
commands.push(
|
|
21047
|
-
`openxiangda release app-finalize --staged-resources-json .openxiangda/releases/${changeId || 'change'}/staged-resources.json --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
|
|
21048
|
-
);
|
|
21049
|
-
}
|
|
21050
|
-
return commands;
|
|
21051
|
-
}
|
|
21052
|
-
|
|
21053
|
-
const onlyTargets = [
|
|
21054
|
-
...targets.forms.map(code => `forms/${code}`),
|
|
21055
|
-
...targets.pages.map(code => `pages/${code}`),
|
|
21056
|
-
];
|
|
21057
|
-
if (onlyTargets.length > 0) {
|
|
21058
|
-
commands.push(
|
|
21059
|
-
`openxiangda workspace publish --profile ${profileArg} --only ${onlyTargets.join(',')} --skip-resources${
|
|
21060
|
-
changeId ? ` --change ${changeId}` : ''
|
|
21061
|
-
}`
|
|
21062
|
-
);
|
|
21063
|
-
}
|
|
21064
|
-
if (hasResourceTargets) {
|
|
21065
|
-
commands.push(...buildTargetedResourceReleaseCommands(targets, profileArg, changeId));
|
|
21066
|
-
}
|
|
21067
|
-
return commands;
|
|
21068
|
-
}
|
|
21069
|
-
|
|
21070
21916
|
function buildWorkspaceReleasePlan(options = {}) {
|
|
21071
21917
|
const configText = readWorkspaceConfigText();
|
|
21072
21918
|
const runtimeMode = getWorkspaceRuntimeModeFromConfig(configText);
|
|
@@ -21095,7 +21941,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21095
21941
|
baseRef: options.since || 'HEAD',
|
|
21096
21942
|
});
|
|
21097
21943
|
let dependencyImpact = {};
|
|
21098
|
-
if (changeScope) {
|
|
21944
|
+
if (changeScope && scopeNeedsBackendDependencyAnalysis(changeScope)) {
|
|
21099
21945
|
const dependencyTargets = getWorkspaceImpactedResourceTargets(files);
|
|
21100
21946
|
dependencyImpact = Object.fromEntries(
|
|
21101
21947
|
['functions', 'automations', 'workflows', 'jsCodeNodes'].map(key => [
|
|
@@ -21110,9 +21956,17 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21110
21956
|
// dozens of unrelated resources. Callers may opt in when the intended
|
|
21111
21957
|
// change genuinely requires the whole dependency closure.
|
|
21112
21958
|
if (options.expandDependencies) {
|
|
21113
|
-
|
|
21114
|
-
|
|
21115
|
-
|
|
21959
|
+
for (const key of [
|
|
21960
|
+
'functions',
|
|
21961
|
+
'automations',
|
|
21962
|
+
'workflows',
|
|
21963
|
+
'jsCodeNodes',
|
|
21964
|
+
]) {
|
|
21965
|
+
targets[key] = unique([
|
|
21966
|
+
...(targets[key] || []),
|
|
21967
|
+
...(dependencyTargets[key] || []),
|
|
21968
|
+
]).sort();
|
|
21969
|
+
}
|
|
21116
21970
|
}
|
|
21117
21971
|
}
|
|
21118
21972
|
if (options.includeResources) targets.resources = true;
|
|
@@ -21138,6 +21992,10 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21138
21992
|
name: issue.rule,
|
|
21139
21993
|
message: `${issue.file}:${issue.line} ${issue.message}`,
|
|
21140
21994
|
}));
|
|
21995
|
+
const normalizedReleaseTargets = normalizeReleaseTargets(
|
|
21996
|
+
targets,
|
|
21997
|
+
runtimeMode
|
|
21998
|
+
);
|
|
21141
21999
|
const generatedCommands = buildWorkspaceReleaseCommands(
|
|
21142
22000
|
targets,
|
|
21143
22001
|
runtimeMode,
|
|
@@ -21226,6 +22084,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21226
22084
|
since: options.changed ? options.since || undefined : undefined,
|
|
21227
22085
|
changedFiles: changeScope || options.changed ? files : undefined,
|
|
21228
22086
|
targets,
|
|
22087
|
+
activationTargets: normalizedReleaseTargets.activationTargets,
|
|
21229
22088
|
publishOrder:
|
|
21230
22089
|
runtimeMode === 'react-spa'
|
|
21231
22090
|
? ['forms', 'resources', 'runtime']
|