openxiangda 1.0.172 → 1.0.173
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 +4 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +695 -102
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +46 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +178 -41
- package/openxiangda-skills/SKILL.md +4 -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 +4 -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,434 @@ 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 parseChildJsonOutput(stdout) {
|
|
953
|
+
const text = String(stdout || '').trim();
|
|
954
|
+
if (!text) return null;
|
|
955
|
+
try {
|
|
956
|
+
return JSON.parse(text);
|
|
957
|
+
} catch {
|
|
958
|
+
const lines = text.split(/\r?\n/);
|
|
959
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
960
|
+
const candidate = lines.slice(index).join('\n').trim();
|
|
961
|
+
if (!candidate.startsWith('{') && !candidate.startsWith('[')) continue;
|
|
962
|
+
try {
|
|
963
|
+
return JSON.parse(candidate);
|
|
964
|
+
} catch {
|
|
965
|
+
// Continue until the first complete JSON payload is found.
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return { output: maskText(text).slice(-4000) };
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function runOpenXiangdaChild(args, options = {}) {
|
|
973
|
+
return new Promise((resolve, reject) => {
|
|
974
|
+
const bin = path.join(__dirname, '..', 'bin', 'openxiangda.js');
|
|
975
|
+
const child = spawn(process.execPath, [bin, ...args, '--json'], {
|
|
976
|
+
cwd: process.cwd(),
|
|
977
|
+
env: process.env,
|
|
978
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
979
|
+
});
|
|
980
|
+
let stdout = '';
|
|
981
|
+
let stderr = '';
|
|
982
|
+
child.stdout.on('data', chunk => {
|
|
983
|
+
stdout += String(chunk);
|
|
984
|
+
});
|
|
985
|
+
child.stderr.on('data', chunk => {
|
|
986
|
+
const text = String(chunk);
|
|
987
|
+
stderr += text;
|
|
988
|
+
if (!options.quiet) warn(maskText(text).trimEnd());
|
|
989
|
+
});
|
|
990
|
+
child.on('error', reject);
|
|
991
|
+
child.on('close', code => {
|
|
992
|
+
if (code === 0) {
|
|
993
|
+
resolve(parseChildJsonOutput(stdout));
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const message = maskText(
|
|
997
|
+
String(stderr || stdout || `openxiangda ${args.join(' ')} failed`)
|
|
998
|
+
).trim();
|
|
999
|
+
const error = new Error(message);
|
|
1000
|
+
error.code =
|
|
1001
|
+
message.match(/\b([A-Z][A-Z0-9_]{3,})\b/)?.[1] ||
|
|
1002
|
+
'RELEASE_STEP_FAILED';
|
|
1003
|
+
error.exitCode = code;
|
|
1004
|
+
reject(error);
|
|
1005
|
+
});
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function normalizeReleaseWaitSeconds(flags) {
|
|
1010
|
+
const raw =
|
|
1011
|
+
readStringFlag(flags, 'wait-seconds') ||
|
|
1012
|
+
readStringFlag(flags, 'lease-wait-seconds');
|
|
1013
|
+
if (raw === '') return 1800;
|
|
1014
|
+
const value = Number(raw);
|
|
1015
|
+
if (!Number.isFinite(value) || value < 0 || value > 86400) {
|
|
1016
|
+
fail('--wait-seconds 必须是 0-86400 之间的秒数');
|
|
1017
|
+
}
|
|
1018
|
+
return Math.floor(value);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function waitForPublishLeaseAvailability(config, target, flags = {}) {
|
|
1022
|
+
if (getUsableStoredPublishLease(target)) return { waitedMs: 0 };
|
|
1023
|
+
const waitSeconds = normalizeReleaseWaitSeconds(flags);
|
|
1024
|
+
const startedAt = Date.now();
|
|
1025
|
+
const deadline = startedAt + waitSeconds * 1000;
|
|
1026
|
+
const clientSessionId = createPublishClientSessionId();
|
|
1027
|
+
let attempts = 0;
|
|
1028
|
+
while (true) {
|
|
1029
|
+
const remote = await fetchPublishLeaseStatus(
|
|
1030
|
+
config,
|
|
1031
|
+
target,
|
|
1032
|
+
clientSessionId
|
|
1033
|
+
);
|
|
1034
|
+
if (!remote?.active) {
|
|
1035
|
+
return { waitedMs: Date.now() - startedAt, attempts, remote };
|
|
1036
|
+
}
|
|
1037
|
+
const remoteHolder =
|
|
1038
|
+
(typeof remote?.holder === 'string' ? remote.holder : null) ||
|
|
1039
|
+
remote?.holder?.changeId ||
|
|
1040
|
+
remote?.changeId ||
|
|
1041
|
+
remote?.holder?.clientSessionId ||
|
|
1042
|
+
'another release';
|
|
1043
|
+
if (Date.now() >= deadline) {
|
|
1044
|
+
fail(
|
|
1045
|
+
`PUBLISH_LEASE_WAIT_TIMEOUT: ${target.appType} 仍由 ${remoteHolder} 发布中;已等待 ${waitSeconds}s`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
attempts += 1;
|
|
1049
|
+
const suggestedSeconds = Number(
|
|
1050
|
+
remote?.retryAfterSeconds || remote?.retryAfter || 5
|
|
1051
|
+
);
|
|
1052
|
+
const delayMs = Math.min(
|
|
1053
|
+
Math.max(Number.isFinite(suggestedSeconds) ? suggestedSeconds * 1000 : 5000, 1000),
|
|
1054
|
+
30000,
|
|
1055
|
+
Math.max(1000, deadline - Date.now())
|
|
1056
|
+
);
|
|
1057
|
+
warn(
|
|
1058
|
+
`发布队列等待中: app=${target.appType} holder=${remoteHolder} retry=${Math.ceil(delayMs / 1000)}s`
|
|
1059
|
+
);
|
|
1060
|
+
await sleep(delayMs);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
1065
|
+
const changeId = readStringFlag(flags, 'change');
|
|
1066
|
+
if (!changeId) {
|
|
1067
|
+
fail('用法: openxiangda release publish --change <id> --profile <name>');
|
|
1068
|
+
}
|
|
1069
|
+
const plan = buildWorkspaceReleasePlan({
|
|
1070
|
+
changed: true,
|
|
1071
|
+
since: flags.since,
|
|
1072
|
+
profile: target.profileName,
|
|
1073
|
+
skipSdd: true,
|
|
1074
|
+
changeId,
|
|
1075
|
+
expandDependencies: Boolean(flags['expand-dependencies']),
|
|
1076
|
+
});
|
|
1077
|
+
if (plan.errors.length > 0) {
|
|
1078
|
+
fail(
|
|
1079
|
+
`RELEASE_PLAN_INVALID: ${plan.errors
|
|
1080
|
+
.map(error => error.message || error.name)
|
|
1081
|
+
.join('; ')}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const steps = buildWorkspaceReleaseSteps(
|
|
1085
|
+
plan.targets,
|
|
1086
|
+
plan.workspace.runtimeMode,
|
|
1087
|
+
target.profileName,
|
|
1088
|
+
changeId
|
|
1089
|
+
);
|
|
1090
|
+
const planHash = releasePlanHash({
|
|
1091
|
+
schemaVersion: 'openxiangda_release_execution_v1',
|
|
1092
|
+
appType: target.appType,
|
|
1093
|
+
profile: target.profileName,
|
|
1094
|
+
changeId,
|
|
1095
|
+
runtimeMode: plan.workspace.runtimeMode,
|
|
1096
|
+
targets: plan.activationTargets,
|
|
1097
|
+
commands: steps.map(step => step.command),
|
|
1098
|
+
});
|
|
1099
|
+
if (flags['dry-run']) {
|
|
1100
|
+
return {
|
|
1101
|
+
dryRun: true,
|
|
1102
|
+
appType: target.appType,
|
|
1103
|
+
profile: target.profileName,
|
|
1104
|
+
changeId,
|
|
1105
|
+
planHash,
|
|
1106
|
+
targets: plan.targets,
|
|
1107
|
+
activationTargets: plan.activationTargets,
|
|
1108
|
+
steps,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const verification = verifySddChange({
|
|
1112
|
+
cwd: process.cwd(),
|
|
1113
|
+
changeId,
|
|
1114
|
+
releasePlan: {
|
|
1115
|
+
...plan,
|
|
1116
|
+
commands: steps.map(step => step.command),
|
|
1117
|
+
},
|
|
1118
|
+
writeMetadata: true,
|
|
1119
|
+
stage: 'prepublish',
|
|
1120
|
+
});
|
|
1121
|
+
if (!verification.passed) {
|
|
1122
|
+
fail(
|
|
1123
|
+
`SDD_PREPUBLISH_FAILED: ${verification.errors
|
|
1124
|
+
.map(error => error.message)
|
|
1125
|
+
.join('; ')}`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
if (steps.length === 0) {
|
|
1129
|
+
return {
|
|
1130
|
+
completed: true,
|
|
1131
|
+
noOp: true,
|
|
1132
|
+
appType: target.appType,
|
|
1133
|
+
profile: target.profileName,
|
|
1134
|
+
changeId,
|
|
1135
|
+
planHash,
|
|
1136
|
+
execution: null,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
const existing = readReleaseExecution(changeId);
|
|
1140
|
+
if (
|
|
1141
|
+
existing &&
|
|
1142
|
+
(existing.planHash !== planHash ||
|
|
1143
|
+
existing.appType !== target.appType ||
|
|
1144
|
+
existing.profile !== target.profileName)
|
|
1145
|
+
) {
|
|
1146
|
+
fail(
|
|
1147
|
+
'RELEASE_EXECUTION_PLAN_CHANGED: 已存在不同计划的执行日志;请先完成或人工核对旧发布,禁止静默覆盖'
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
if (
|
|
1151
|
+
existing?.status === 'write-review-required' &&
|
|
1152
|
+
!flags['resume-after-review']
|
|
1153
|
+
) {
|
|
1154
|
+
fail(
|
|
1155
|
+
'RELEASE_WRITE_REVIEW_REQUIRED: 上次写请求结果不确定;请先只读核对 staged release,确认可重试后追加 --resume-after-review'
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
const now = new Date().toISOString();
|
|
1159
|
+
const execution = existing || {
|
|
1160
|
+
schemaVersion: 'openxiangda_release_execution_v1',
|
|
1161
|
+
appType: target.appType,
|
|
1162
|
+
profile: target.profileName,
|
|
1163
|
+
changeId,
|
|
1164
|
+
planHash,
|
|
1165
|
+
status: 'planned',
|
|
1166
|
+
createdAt: now,
|
|
1167
|
+
updatedAt: now,
|
|
1168
|
+
stagedWriteOccurred: false,
|
|
1169
|
+
writeAttempted: false,
|
|
1170
|
+
steps: [
|
|
1171
|
+
{
|
|
1172
|
+
id: 'lease-and-capture',
|
|
1173
|
+
command: `openxiangda release begin --change ${changeId} --profile ${target.profileName} --freeze-app-capture`,
|
|
1174
|
+
status: 'pending',
|
|
1175
|
+
},
|
|
1176
|
+
...steps.map(step => ({
|
|
1177
|
+
id: step.id,
|
|
1178
|
+
command: step.command,
|
|
1179
|
+
stagedKind: step.stagedKind,
|
|
1180
|
+
status: 'pending',
|
|
1181
|
+
})),
|
|
1182
|
+
{
|
|
1183
|
+
id: 'release-lease',
|
|
1184
|
+
command: `openxiangda release end --change ${changeId} --profile ${target.profileName}`,
|
|
1185
|
+
status: 'pending',
|
|
1186
|
+
},
|
|
1187
|
+
],
|
|
1188
|
+
};
|
|
1189
|
+
if (execution.status === 'completed') {
|
|
1190
|
+
return {
|
|
1191
|
+
completed: true,
|
|
1192
|
+
resumed: true,
|
|
1193
|
+
appType: target.appType,
|
|
1194
|
+
profile: target.profileName,
|
|
1195
|
+
changeId,
|
|
1196
|
+
planHash,
|
|
1197
|
+
executionFile: path
|
|
1198
|
+
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1199
|
+
.replace(/\\/g, '/'),
|
|
1200
|
+
execution,
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
execution.status = 'running';
|
|
1204
|
+
execution.updatedAt = new Date().toISOString();
|
|
1205
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1206
|
+
|
|
1207
|
+
const runnable = [
|
|
1208
|
+
{
|
|
1209
|
+
id: 'lease-and-capture',
|
|
1210
|
+
args: [
|
|
1211
|
+
'release',
|
|
1212
|
+
'begin',
|
|
1213
|
+
'--change',
|
|
1214
|
+
changeId,
|
|
1215
|
+
'--profile',
|
|
1216
|
+
target.profileName,
|
|
1217
|
+
'--freeze-app-capture',
|
|
1218
|
+
],
|
|
1219
|
+
},
|
|
1220
|
+
...steps,
|
|
1221
|
+
{
|
|
1222
|
+
id: 'release-lease',
|
|
1223
|
+
args: [
|
|
1224
|
+
'release',
|
|
1225
|
+
'end',
|
|
1226
|
+
'--change',
|
|
1227
|
+
changeId,
|
|
1228
|
+
'--profile',
|
|
1229
|
+
target.profileName,
|
|
1230
|
+
],
|
|
1231
|
+
},
|
|
1232
|
+
];
|
|
1233
|
+
try {
|
|
1234
|
+
for (const step of runnable) {
|
|
1235
|
+
const journalStep = execution.steps.find(item => item.id === step.id);
|
|
1236
|
+
if (journalStep?.status === 'completed') continue;
|
|
1237
|
+
if (step.id === 'lease-and-capture') {
|
|
1238
|
+
await waitForPublishLeaseAvailability(config, target, flags);
|
|
1239
|
+
}
|
|
1240
|
+
journalStep.status = 'running';
|
|
1241
|
+
journalStep.startedAt = new Date().toISOString();
|
|
1242
|
+
if (
|
|
1243
|
+
step.id !== 'lease-and-capture' &&
|
|
1244
|
+
step.id !== 'release-lease' &&
|
|
1245
|
+
step.writes !== false
|
|
1246
|
+
) {
|
|
1247
|
+
execution.writeAttempted = true;
|
|
1248
|
+
}
|
|
1249
|
+
execution.updatedAt = journalStep.startedAt;
|
|
1250
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1251
|
+
warn(`发布步骤开始: ${step.id}`);
|
|
1252
|
+
const result = await runOpenXiangdaChild(step.args);
|
|
1253
|
+
journalStep.status = 'completed';
|
|
1254
|
+
journalStep.completedAt = new Date().toISOString();
|
|
1255
|
+
journalStep.result = summarizeReleaseStepResult(result);
|
|
1256
|
+
if (step.stagedKind) execution.stagedWriteOccurred = true;
|
|
1257
|
+
execution.updatedAt = journalStep.completedAt;
|
|
1258
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1259
|
+
}
|
|
1260
|
+
execution.status = 'completed';
|
|
1261
|
+
execution.completedAt = new Date().toISOString();
|
|
1262
|
+
execution.updatedAt = execution.completedAt;
|
|
1263
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1264
|
+
return {
|
|
1265
|
+
completed: true,
|
|
1266
|
+
resumed: Boolean(existing),
|
|
1267
|
+
appType: target.appType,
|
|
1268
|
+
profile: target.profileName,
|
|
1269
|
+
changeId,
|
|
1270
|
+
planHash,
|
|
1271
|
+
executionFile: path
|
|
1272
|
+
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1273
|
+
.replace(/\\/g, '/'),
|
|
1274
|
+
execution,
|
|
1275
|
+
};
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
const runningStep = execution.steps.find(step => step.status === 'running');
|
|
1278
|
+
if (runningStep) {
|
|
1279
|
+
runningStep.status = 'failed';
|
|
1280
|
+
runningStep.failedAt = new Date().toISOString();
|
|
1281
|
+
runningStep.error = {
|
|
1282
|
+
code: error.code || 'RELEASE_STEP_FAILED',
|
|
1283
|
+
message: maskText(error.message).slice(0, 1000),
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
execution.status = execution.stagedWriteOccurred
|
|
1287
|
+
? 'staged-resumable'
|
|
1288
|
+
: execution.writeAttempted
|
|
1289
|
+
? 'write-review-required'
|
|
1290
|
+
: 'failed';
|
|
1291
|
+
execution.updatedAt = new Date().toISOString();
|
|
1292
|
+
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1293
|
+
if (!execution.writeAttempted) {
|
|
1294
|
+
try {
|
|
1295
|
+
await runOpenXiangdaChild(
|
|
1296
|
+
[
|
|
1297
|
+
'release',
|
|
1298
|
+
'end',
|
|
1299
|
+
'--change',
|
|
1300
|
+
changeId,
|
|
1301
|
+
'--profile',
|
|
1302
|
+
target.profileName,
|
|
1303
|
+
],
|
|
1304
|
+
{ quiet: true }
|
|
1305
|
+
);
|
|
1306
|
+
} catch {
|
|
1307
|
+
// The original error remains authoritative; lease cleanup is best effort.
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
throw error;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
887
1314
|
async function release(args) {
|
|
888
1315
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
889
1316
|
const { flags, positional } = parseArgs(rest);
|
|
890
1317
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
891
1318
|
print([
|
|
892
|
-
'用法: openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
1319
|
+
'用法: openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
893
1320
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
894
1321
|
' 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
1322
|
'常用流程:',
|
|
1323
|
+
' openxiangda release publish --change <id> --profile <name>',
|
|
896
1324
|
' openxiangda release begin --change <id> --profile <name>',
|
|
897
1325
|
' openxiangda resource publish function --only <code> --change <id> --profile <name>',
|
|
898
1326
|
' openxiangda runtime deploy --change <id> --profile <name>',
|
|
@@ -903,6 +1331,7 @@ async function release(args) {
|
|
|
903
1331
|
' openxiangda release backend-rollback <releaseId> --change <id> --reason "..." --profile <name>',
|
|
904
1332
|
' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
|
|
905
1333
|
'说明:',
|
|
1334
|
+
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
906
1335
|
' - begin 获取应用级单写者 promotion lease;不同工作区仍可并行开发和验证。',
|
|
907
1336
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
908
1337
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
@@ -941,6 +1370,16 @@ async function release(args) {
|
|
|
941
1370
|
flags
|
|
942
1371
|
);
|
|
943
1372
|
}
|
|
1373
|
+
if (subcommand === 'publish') {
|
|
1374
|
+
const result = await publishWorkspaceRelease(config, target, flags);
|
|
1375
|
+
if (flags.json) return writeJson(result);
|
|
1376
|
+
print(
|
|
1377
|
+
result.dryRun
|
|
1378
|
+
? `发布计划已生成(未写入): ${result.changeId} steps=${result.steps.length}`
|
|
1379
|
+
: `应用原子发布完成: ${result.appType} change=${result.changeId}`
|
|
1380
|
+
);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
944
1383
|
const leaseIdFromArgs = positional[0] || readStringFlag(flags, 'lease-id');
|
|
945
1384
|
|
|
946
1385
|
if (subcommand === 'status') {
|
|
@@ -1040,15 +1479,56 @@ async function release(args) {
|
|
|
1040
1479
|
throw error;
|
|
1041
1480
|
}
|
|
1042
1481
|
const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1482
|
+
let frozenCapture = null;
|
|
1483
|
+
let releaseSessionFile = null;
|
|
1484
|
+
let stagedResourcesFile = null;
|
|
1485
|
+
try {
|
|
1486
|
+
if (flags['freeze-app-capture']) {
|
|
1487
|
+
frozenCapture = await fetchAppReleaseCapture(config, target, flags);
|
|
1488
|
+
releaseSessionFile = saveFrozenAppReleaseCapture(
|
|
1489
|
+
target,
|
|
1490
|
+
changeId,
|
|
1491
|
+
finalizedBaseline,
|
|
1492
|
+
lease,
|
|
1493
|
+
frozenCapture
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
stagedResourcesFile = newReleaseLifecycle
|
|
1497
|
+
? initializeStagedAppReleaseResources(target, changeId)
|
|
1498
|
+
: null;
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
if (newReleaseLifecycle) {
|
|
1501
|
+
try {
|
|
1502
|
+
await requestWithAuth(
|
|
1503
|
+
config,
|
|
1504
|
+
target.profileName,
|
|
1505
|
+
publishLeaseApiPath(
|
|
1506
|
+
target,
|
|
1507
|
+
`/${encodeURIComponent(lease.leaseId)}/release`
|
|
1508
|
+
),
|
|
1509
|
+
{ method: 'POST', body: { completion: 'local-prepare-failed' } }
|
|
1510
|
+
);
|
|
1511
|
+
} catch {
|
|
1512
|
+
// Preserve the local preparation failure as the primary error.
|
|
1513
|
+
}
|
|
1514
|
+
clearPublishLease(target, lease.leaseId);
|
|
1515
|
+
clearChangeBaseline(target, finalizedBaseline?.baselineId);
|
|
1516
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
1517
|
+
}
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1046
1520
|
const result = {
|
|
1047
1521
|
...lease,
|
|
1048
1522
|
baselineId: finalizedBaseline?.baselineId,
|
|
1049
1523
|
baselineHeadDigest: finalizedBaseline?.headDigest,
|
|
1050
1524
|
sourceBase: finalizedBaseline?.sourceBase,
|
|
1051
1525
|
releaseSourceRevision: finalizedBaseline?.releaseSourceRevision,
|
|
1526
|
+
...(frozenCapture
|
|
1527
|
+
? {
|
|
1528
|
+
captureHash: frozenCapture.captureHash,
|
|
1529
|
+
releaseSessionFile,
|
|
1530
|
+
}
|
|
1531
|
+
: {}),
|
|
1052
1532
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
1053
1533
|
};
|
|
1054
1534
|
if (flags.json) return writeJson(result);
|
|
@@ -1191,7 +1671,7 @@ async function release(args) {
|
|
|
1191
1671
|
}
|
|
1192
1672
|
}
|
|
1193
1673
|
|
|
1194
|
-
fail('用法: openxiangda release begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
|
|
1674
|
+
fail('用法: openxiangda release publish|begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
|
|
1195
1675
|
}
|
|
1196
1676
|
|
|
1197
1677
|
async function fetchPublishLeaseStatus(config, target, clientSessionId) {
|
|
@@ -1460,6 +1940,74 @@ async function fetchAppReleaseCapture(config, target, flags = {}) {
|
|
|
1460
1940
|
return clonePlainJson(capture);
|
|
1461
1941
|
}
|
|
1462
1942
|
|
|
1943
|
+
function releaseSessionFile(changeId) {
|
|
1944
|
+
return path.join(releaseExecutionDir(changeId), 'release-session.json');
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function saveFrozenAppReleaseCapture(
|
|
1948
|
+
target,
|
|
1949
|
+
changeId,
|
|
1950
|
+
baseline,
|
|
1951
|
+
lease,
|
|
1952
|
+
capture
|
|
1953
|
+
) {
|
|
1954
|
+
const file = releaseSessionFile(changeId);
|
|
1955
|
+
writePrivateJsonAtomic(file, {
|
|
1956
|
+
schemaVersion: 'openxiangda_release_session_v1',
|
|
1957
|
+
appType: target.appType,
|
|
1958
|
+
profile: target.profileName,
|
|
1959
|
+
changeId,
|
|
1960
|
+
baselineId: baseline?.baselineId || baseline?.id || null,
|
|
1961
|
+
leaseId: lease?.leaseId || lease?.id || null,
|
|
1962
|
+
sourceRevision: baseline?.releaseSourceRevision || null,
|
|
1963
|
+
capturedAt: new Date().toISOString(),
|
|
1964
|
+
capture,
|
|
1965
|
+
});
|
|
1966
|
+
return path.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function readFrozenAppReleaseCapture(target, flags = {}) {
|
|
1970
|
+
const changeId = readStringFlag(flags, 'change');
|
|
1971
|
+
if (!changeId) return null;
|
|
1972
|
+
const file = releaseSessionFile(changeId);
|
|
1973
|
+
if (!fs.existsSync(file)) return null;
|
|
1974
|
+
let session;
|
|
1975
|
+
try {
|
|
1976
|
+
session = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
fail(`RELEASE_SESSION_INVALID: ${error.message}`);
|
|
1979
|
+
}
|
|
1980
|
+
if (
|
|
1981
|
+
session?.schemaVersion !== 'openxiangda_release_session_v1' ||
|
|
1982
|
+
session?.appType !== target.appType ||
|
|
1983
|
+
session?.profile !== target.profileName ||
|
|
1984
|
+
session?.changeId !== changeId
|
|
1985
|
+
) {
|
|
1986
|
+
fail(
|
|
1987
|
+
'RELEASE_SESSION_CONTEXT_MISMATCH: 冻结的 App capture 与当前 app/profile/change 不一致'
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
const capture = session.capture;
|
|
1991
|
+
if (
|
|
1992
|
+
!capture ||
|
|
1993
|
+
!Array.isArray(capture.resources) ||
|
|
1994
|
+
!/^[a-f0-9]{64}$/i.test(String(capture.captureHash || ''))
|
|
1995
|
+
) {
|
|
1996
|
+
fail('RELEASE_SESSION_CAPTURE_INVALID: 冻结的 App capture 不完整');
|
|
1997
|
+
}
|
|
1998
|
+
const expectedCaptureHash = readStringFlag(flags, 'expected-capture-hash');
|
|
1999
|
+
if (
|
|
2000
|
+
expectedCaptureHash &&
|
|
2001
|
+
expectedCaptureHash.toLowerCase() !==
|
|
2002
|
+
String(capture.captureHash).toLowerCase()
|
|
2003
|
+
) {
|
|
2004
|
+
fail(
|
|
2005
|
+
`APP_RELEASE_CAPTURE_MOVED: 期望 capture ${expectedCaptureHash},冻结为 ${capture.captureHash}`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
return clonePlainJson(capture);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1463
2011
|
function appReleaseWriteBody(releaseContext, extra = {}) {
|
|
1464
2012
|
return {
|
|
1465
2013
|
...releaseContext.control,
|
|
@@ -1790,7 +2338,13 @@ function assertStagedAppReleaseScope(target, flags, resources) {
|
|
|
1790
2338
|
}
|
|
1791
2339
|
throw error;
|
|
1792
2340
|
}
|
|
1793
|
-
const
|
|
2341
|
+
const runtimeMode = getWorkspaceRuntimeModeFromConfig(
|
|
2342
|
+
readWorkspaceConfigText()
|
|
2343
|
+
);
|
|
2344
|
+
const { activationTargets: targets } = normalizeReleaseTargets(
|
|
2345
|
+
scope.targets || {},
|
|
2346
|
+
runtimeMode
|
|
2347
|
+
);
|
|
1794
2348
|
const normalized = normalizeStagedAppReleaseResources(resources);
|
|
1795
2349
|
const byKind = kind => normalized.filter(resource => resource.kind === kind);
|
|
1796
2350
|
const expectedBackendSelectors = [
|
|
@@ -2236,7 +2790,9 @@ async function runAppReleaseCommand(
|
|
|
2236
2790
|
flags,
|
|
2237
2791
|
'App Release finalize'
|
|
2238
2792
|
);
|
|
2239
|
-
const capture =
|
|
2793
|
+
const capture =
|
|
2794
|
+
readFrozenAppReleaseCapture(target, flags) ||
|
|
2795
|
+
(await fetchAppReleaseCapture(config, target, flags));
|
|
2240
2796
|
const frozenResources = overlayAppReleaseResources(
|
|
2241
2797
|
capture.resources,
|
|
2242
2798
|
stagedResources
|
|
@@ -8761,6 +9317,9 @@ async function resource(args) {
|
|
|
8761
9317
|
fail('resource publish --only/--code 不能与 --prune 同时使用,避免删除未选中的资源');
|
|
8762
9318
|
}
|
|
8763
9319
|
const manifest = loadWorkspaceResources({ typeFilters, codeFilters });
|
|
9320
|
+
if (subcommand !== 'typegen') {
|
|
9321
|
+
await attachLocalFormSchemas(manifest);
|
|
9322
|
+
}
|
|
8764
9323
|
if (
|
|
8765
9324
|
subcommand === 'plan' &&
|
|
8766
9325
|
process.env.OPENXIANGDA_GIT_BASE_RESOURCE_PLAN === '1'
|
|
@@ -9560,12 +10119,23 @@ async function runtime(args) {
|
|
|
9560
10119
|
}
|
|
9561
10120
|
|
|
9562
10121
|
async function loadRuntimeHeadApp(config, target) {
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
10122
|
+
try {
|
|
10123
|
+
return (
|
|
10124
|
+
(await requestWithAuth(
|
|
10125
|
+
config,
|
|
10126
|
+
target.profileName,
|
|
10127
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases/head`
|
|
10128
|
+
)) || {}
|
|
10129
|
+
);
|
|
10130
|
+
} catch (error) {
|
|
10131
|
+
if (!isHttpNotFound(error)) throw error;
|
|
10132
|
+
const snapshot = await requestWithAuth(
|
|
10133
|
+
config,
|
|
10134
|
+
target.profileName,
|
|
10135
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/snapshot`
|
|
10136
|
+
);
|
|
10137
|
+
return snapshot?.app || snapshot?.application || snapshot || {};
|
|
10138
|
+
}
|
|
9569
10139
|
}
|
|
9570
10140
|
|
|
9571
10141
|
function assertRuntimeSourceRevisionStable(beforeBuild, afterBuild) {
|
|
@@ -10180,7 +10750,7 @@ async function commands(args) {
|
|
|
10180
10750
|
'open-api credential list|get|create|update|rotate-secret [id]',
|
|
10181
10751
|
'design gates|template|review [--topic code]',
|
|
10182
10752
|
'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',
|
|
10753
|
+
'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
10754
|
'env',
|
|
10185
10755
|
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
10186
10756
|
'app list|create|snapshot',
|
|
@@ -11646,6 +12216,84 @@ function loadWorkspaceResources(options = {}) {
|
|
|
11646
12216
|
return manifest;
|
|
11647
12217
|
}
|
|
11648
12218
|
|
|
12219
|
+
async function attachLocalFormSchemas(manifest) {
|
|
12220
|
+
if (
|
|
12221
|
+
Array.isArray(manifest.resourceTypeFilters) &&
|
|
12222
|
+
!manifest.resourceTypeFilters.includes('formSettings')
|
|
12223
|
+
) {
|
|
12224
|
+
return manifest;
|
|
12225
|
+
}
|
|
12226
|
+
const selectors = Array.isArray(manifest.__resourceCodeSelectors)
|
|
12227
|
+
? manifest.__resourceCodeSelectors.filter(selector =>
|
|
12228
|
+
resourceCodeSelectorAppliesToKey(selector, 'formSettings')
|
|
12229
|
+
)
|
|
12230
|
+
: [];
|
|
12231
|
+
const candidateCodes = unique([
|
|
12232
|
+
...(manifest.formSettings || []).map(getResourceItemCode),
|
|
12233
|
+
...selectors.map(selector => selector.code),
|
|
12234
|
+
]).filter(Boolean);
|
|
12235
|
+
if (candidateCodes.length === 0) return manifest;
|
|
12236
|
+
|
|
12237
|
+
const loadedCodes = new Set();
|
|
12238
|
+
for (const code of candidateCodes) {
|
|
12239
|
+
let localSchema;
|
|
12240
|
+
try {
|
|
12241
|
+
localSchema = await loadLocalFormSchema(process.cwd(), code);
|
|
12242
|
+
} catch (error) {
|
|
12243
|
+
const wrapped = new Error(
|
|
12244
|
+
`FORM_SCHEMA_LOAD_FAILED: ${code}: ${error.message}`
|
|
12245
|
+
);
|
|
12246
|
+
wrapped.code = 'FORM_SCHEMA_LOAD_FAILED';
|
|
12247
|
+
throw wrapped;
|
|
12248
|
+
}
|
|
12249
|
+
if (!localSchema) continue;
|
|
12250
|
+
loadedCodes.add(code);
|
|
12251
|
+
const existing = (manifest.formSettings || []).find(
|
|
12252
|
+
item => getResourceItemCode(item) === code
|
|
12253
|
+
);
|
|
12254
|
+
const merged = {
|
|
12255
|
+
...(existing || {}),
|
|
12256
|
+
code,
|
|
12257
|
+
formCode: existing?.formCode || code,
|
|
12258
|
+
name: existing?.name ?? localSchema.name,
|
|
12259
|
+
schema: localSchema.schema,
|
|
12260
|
+
packages: localSchema.packages,
|
|
12261
|
+
formType: existing?.formType ?? localSchema.formType,
|
|
12262
|
+
__source: existing?.__source || path.join(
|
|
12263
|
+
process.cwd(),
|
|
12264
|
+
localSchema.sourcePath
|
|
12265
|
+
),
|
|
12266
|
+
__dir: existing?.__dir || path.dirname(
|
|
12267
|
+
path.join(process.cwd(), localSchema.sourcePath)
|
|
12268
|
+
),
|
|
12269
|
+
__localSchema: {
|
|
12270
|
+
sourcePath: localSchema.sourcePath,
|
|
12271
|
+
fieldCount: localSchema.fieldCount,
|
|
12272
|
+
},
|
|
12273
|
+
};
|
|
12274
|
+
if (existing) {
|
|
12275
|
+
Object.assign(existing, merged);
|
|
12276
|
+
} else {
|
|
12277
|
+
manifest.formSettings = manifest.formSettings || [];
|
|
12278
|
+
manifest.formSettings.push(merged);
|
|
12279
|
+
}
|
|
12280
|
+
}
|
|
12281
|
+
manifest.formSettings = (manifest.formSettings || []).sort((left, right) =>
|
|
12282
|
+
getResourceItemCode(left).localeCompare(getResourceItemCode(right))
|
|
12283
|
+
);
|
|
12284
|
+
if (
|
|
12285
|
+
loadedCodes.size > 0 &&
|
|
12286
|
+
Array.isArray(manifest.unmatchedResourceCodeFilters)
|
|
12287
|
+
) {
|
|
12288
|
+
manifest.unmatchedResourceCodeFilters =
|
|
12289
|
+
manifest.unmatchedResourceCodeFilters.filter(raw => {
|
|
12290
|
+
const code = String(raw || '').replace(/^.*[:/]/, '');
|
|
12291
|
+
return !loadedCodes.has(code);
|
|
12292
|
+
});
|
|
12293
|
+
}
|
|
12294
|
+
return manifest;
|
|
12295
|
+
}
|
|
12296
|
+
|
|
11649
12297
|
function readResourceItems(baseDir, spec, options = {}) {
|
|
11650
12298
|
const items = [];
|
|
11651
12299
|
const selectorEntries = options.selectorEntries || null;
|
|
@@ -20593,6 +21241,16 @@ function getWorkspaceImpactedResourceTargets(files) {
|
|
|
20593
21241
|
return getImpactedResourceTargets(getWorkspaceSourceDependencyIndex(), files);
|
|
20594
21242
|
}
|
|
20595
21243
|
|
|
21244
|
+
function scopeNeedsBackendDependencyAnalysis(scope) {
|
|
21245
|
+
const targets = scope?.targets || {};
|
|
21246
|
+
return (
|
|
21247
|
+
Boolean(targets.resources) ||
|
|
21248
|
+
['functions', 'automations', 'workflows', 'jsCodeNodes'].some(
|
|
21249
|
+
key => (targets[key] || []).length > 0
|
|
21250
|
+
)
|
|
21251
|
+
);
|
|
21252
|
+
}
|
|
21253
|
+
|
|
20596
21254
|
function addCodeResourceTargetFromPath(targets, file, prefix, key, options = {}) {
|
|
20597
21255
|
if (!file.startsWith(prefix)) return false;
|
|
20598
21256
|
const rest = file.slice(prefix.length);
|
|
@@ -20890,8 +21548,11 @@ function fileBelongsToChangeScope(file, scope, runtimeMode) {
|
|
|
20890
21548
|
|
|
20891
21549
|
function getChangeScopedWorkspaceFiles(changeScope, runtimeMode, options = {}) {
|
|
20892
21550
|
const dirtyFiles = getWorkspaceChangedFiles({ since: options.since });
|
|
20893
|
-
const dependencyIndex =
|
|
21551
|
+
const dependencyIndex = scopeNeedsBackendDependencyAnalysis(changeScope)
|
|
21552
|
+
? getWorkspaceSourceDependencyIndex()
|
|
21553
|
+
: null;
|
|
20894
21554
|
const dependencyBelongsToScope = file => {
|
|
21555
|
+
if (!dependencyIndex) return false;
|
|
20895
21556
|
const impacted = getImpactedResourceTargets(dependencyIndex, [file]);
|
|
20896
21557
|
return ['functions', 'automations', 'workflows', 'jsCodeNodes'].some(key =>
|
|
20897
21558
|
(impacted[key] || []).some(code => scopeIncludesCode(changeScope.targets?.[key], code))
|
|
@@ -20986,87 +21647,6 @@ function scanWorkspaceReliabilityIssues(files) {
|
|
|
20986
21647
|
return issues;
|
|
20987
21648
|
}
|
|
20988
21649
|
|
|
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
21650
|
function buildWorkspaceReleasePlan(options = {}) {
|
|
21071
21651
|
const configText = readWorkspaceConfigText();
|
|
21072
21652
|
const runtimeMode = getWorkspaceRuntimeModeFromConfig(configText);
|
|
@@ -21095,7 +21675,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21095
21675
|
baseRef: options.since || 'HEAD',
|
|
21096
21676
|
});
|
|
21097
21677
|
let dependencyImpact = {};
|
|
21098
|
-
if (changeScope) {
|
|
21678
|
+
if (changeScope && scopeNeedsBackendDependencyAnalysis(changeScope)) {
|
|
21099
21679
|
const dependencyTargets = getWorkspaceImpactedResourceTargets(files);
|
|
21100
21680
|
dependencyImpact = Object.fromEntries(
|
|
21101
21681
|
['functions', 'automations', 'workflows', 'jsCodeNodes'].map(key => [
|
|
@@ -21110,9 +21690,17 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21110
21690
|
// dozens of unrelated resources. Callers may opt in when the intended
|
|
21111
21691
|
// change genuinely requires the whole dependency closure.
|
|
21112
21692
|
if (options.expandDependencies) {
|
|
21113
|
-
|
|
21114
|
-
|
|
21115
|
-
|
|
21693
|
+
for (const key of [
|
|
21694
|
+
'functions',
|
|
21695
|
+
'automations',
|
|
21696
|
+
'workflows',
|
|
21697
|
+
'jsCodeNodes',
|
|
21698
|
+
]) {
|
|
21699
|
+
targets[key] = unique([
|
|
21700
|
+
...(targets[key] || []),
|
|
21701
|
+
...(dependencyTargets[key] || []),
|
|
21702
|
+
]).sort();
|
|
21703
|
+
}
|
|
21116
21704
|
}
|
|
21117
21705
|
}
|
|
21118
21706
|
if (options.includeResources) targets.resources = true;
|
|
@@ -21138,6 +21726,10 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21138
21726
|
name: issue.rule,
|
|
21139
21727
|
message: `${issue.file}:${issue.line} ${issue.message}`,
|
|
21140
21728
|
}));
|
|
21729
|
+
const normalizedReleaseTargets = normalizeReleaseTargets(
|
|
21730
|
+
targets,
|
|
21731
|
+
runtimeMode
|
|
21732
|
+
);
|
|
21141
21733
|
const generatedCommands = buildWorkspaceReleaseCommands(
|
|
21142
21734
|
targets,
|
|
21143
21735
|
runtimeMode,
|
|
@@ -21226,6 +21818,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21226
21818
|
since: options.changed ? options.since || undefined : undefined,
|
|
21227
21819
|
changedFiles: changeScope || options.changed ? files : undefined,
|
|
21228
21820
|
targets,
|
|
21821
|
+
activationTargets: normalizedReleaseTargets.activationTargets,
|
|
21229
21822
|
publishOrder:
|
|
21230
21823
|
runtimeMode === 'react-spa'
|
|
21231
21824
|
? ['forms', 'resources', 'runtime']
|