openxiangda 1.0.174 → 1.0.176
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 +35 -2
- package/lib/cli.js +1118 -98
- package/lib/content-cache.js +97 -0
- package/lib/dependency-capsule.js +246 -0
- package/lib/integration-bundle.js +204 -0
- package/lib/policy.js +97 -0
- package/lib/release-explain.js +75 -0
- package/lib/release-plan.js +65 -2
- package/lib/sdd.js +163 -13
- package/lib/task-status.js +267 -0
- package/lib/worktree-owner.js +7 -1
- package/openxiangda-skills/SKILL.md +3 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -2
- package/package.json +19 -3
- package/policy/openxiangda-policy.json +51 -0
- package/templates/openxiangda-react-spa/AGENTS.md +2 -1
- package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -1
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
package/lib/cli.js
CHANGED
|
@@ -70,6 +70,25 @@ const {
|
|
|
70
70
|
normalizeReleaseTargets,
|
|
71
71
|
releasePlanHash,
|
|
72
72
|
} = require('./release-plan');
|
|
73
|
+
const {
|
|
74
|
+
assertIntegrationBundleCommits,
|
|
75
|
+
collectIntegrationTaskResults,
|
|
76
|
+
inspectIntegrationBundleCommits,
|
|
77
|
+
readIntegrationTaskResult,
|
|
78
|
+
recordIntegrationTaskResult,
|
|
79
|
+
} = require('./integration-bundle');
|
|
80
|
+
const {
|
|
81
|
+
readContentCache,
|
|
82
|
+
writeContentCache,
|
|
83
|
+
} = require('./content-cache');
|
|
84
|
+
const { explainReleaseExecution } = require('./release-explain');
|
|
85
|
+
const { buildTaskStatus, findAppReleaseId } = require('./task-status');
|
|
86
|
+
const {
|
|
87
|
+
checkEngineeringPolicyMarkers,
|
|
88
|
+
loadEngineeringPolicy,
|
|
89
|
+
renderEngineeringPolicyMarkdown,
|
|
90
|
+
} = require('./policy');
|
|
91
|
+
const { prepareDependencyCapsule } = require('./dependency-capsule');
|
|
73
92
|
const {
|
|
74
93
|
analyzeManifestSourceDependencies,
|
|
75
94
|
getImpactedResourceTargets,
|
|
@@ -118,6 +137,7 @@ const {
|
|
|
118
137
|
initSddWorkspace,
|
|
119
138
|
proposeSddChange,
|
|
120
139
|
recordSddReleaseSourceRevision,
|
|
140
|
+
renderSddDocumentation,
|
|
121
141
|
syncSddChange,
|
|
122
142
|
verifySddChange,
|
|
123
143
|
} = require('./sdd');
|
|
@@ -178,10 +198,12 @@ async function mainImpl(argv) {
|
|
|
178
198
|
if (command === 'platform') return platform(rest);
|
|
179
199
|
if (command === 'doctor') return doctor(rest);
|
|
180
200
|
if (command === 'contract') return contract(rest);
|
|
201
|
+
if (command === 'policy') return policy(rest);
|
|
181
202
|
if (command === 'open-api') return openApi(rest);
|
|
182
203
|
if (command === 'design') return design(rest);
|
|
183
204
|
if (command === 'sdd') return sdd(rest);
|
|
184
205
|
if (command === 'release') return release(rest);
|
|
206
|
+
if (command === 'task') return task(rest);
|
|
185
207
|
if (command === 'workspace') return workspace(rest);
|
|
186
208
|
if (command === 'app') return app(rest);
|
|
187
209
|
if (command === 'form') return form(rest);
|
|
@@ -224,15 +246,18 @@ Usage:
|
|
|
224
246
|
openxiangda auth status|refresh|logout [--profile name]
|
|
225
247
|
openxiangda doctor [--profile name] [--app-type APP_XXX] [--release] [--json]
|
|
226
248
|
openxiangda contract list|explain <contract-name> [--json]
|
|
249
|
+
openxiangda policy show|check|render [--json]
|
|
227
250
|
openxiangda open-api spec tags|list|describe [operationId|path] [--method post] [--tag name] [--search text] [--json]
|
|
228
251
|
openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
|
|
229
252
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
230
253
|
openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
231
|
-
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
|
|
254
|
+
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
|
|
255
|
+
openxiangda task status --change <id> [--profile name] [--watch] [--json]
|
|
232
256
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
233
257
|
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]
|
|
234
258
|
openxiangda env [--profile name]
|
|
235
259
|
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--profile name --app-type APP_XXX]
|
|
260
|
+
openxiangda workspace prepare [--check] [--force] [--json]
|
|
236
261
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--install]
|
|
237
262
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
238
263
|
openxiangda workspace plan|check [--change id|--changed] [--json]
|
|
@@ -329,6 +354,8 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
329
354
|
'--summary',
|
|
330
355
|
'--unpublish',
|
|
331
356
|
'--value-stdin',
|
|
357
|
+
'--wait',
|
|
358
|
+
'--watch',
|
|
332
359
|
'--write-manifest',
|
|
333
360
|
'--yes',
|
|
334
361
|
'-h',
|
|
@@ -652,19 +679,21 @@ async function sdd(args) {
|
|
|
652
679
|
const { flags, positional } = parseArgs(rest);
|
|
653
680
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
654
681
|
print([
|
|
655
|
-
'用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]',
|
|
682
|
+
'用法: openxiangda sdd init|migrate|propose|quick|ready|bundle|approve|status|context|verify|render|sync|archive [change] [--change id] [--json]',
|
|
656
683
|
'常用流程:',
|
|
657
684
|
' openxiangda sdd init --upgrade',
|
|
658
685
|
' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list --resources',
|
|
659
686
|
' openxiangda sdd quick tune-customer-copy --kind copy --pages customer_list --files src/pages/customer_list/index.tsx --summary "用户确认小范围文案调整"',
|
|
660
687
|
' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
|
|
688
|
+
' openxiangda sdd ready add-customer-list',
|
|
661
689
|
' openxiangda sdd bundle mainline-release --changes add-customer-list,fix-order-log',
|
|
662
690
|
' openxiangda sdd context --change add-customer-list --json',
|
|
663
691
|
' openxiangda sdd verify --change add-customer-list --stage prepublish --json',
|
|
692
|
+
' openxiangda sdd render add-customer-list # 仅在确实需要完整文档时',
|
|
664
693
|
' openxiangda sdd archive --change add-customer-list',
|
|
665
694
|
'说明:',
|
|
666
695
|
' - SDD 记录保存在 openspec/,不依赖官方 OpenSpec CLI。',
|
|
667
|
-
' -
|
|
696
|
+
' - 新工作区默认只维护 change.json/coverage.json/release.json;proposal/specs/design/tasks/evidence 由 sdd render 按需生成。',
|
|
668
697
|
' - 高风险发布需要 approved change 的 coverage 覆盖本次 forms/pages/functions/automations/workflows/JS_CODE/resources/runtime 目标和高风险文件。',
|
|
669
698
|
' - --change 使用该 change 的 coverage/release 与范围内 dirty files;不会吸收其他并行会话的全局变更。',
|
|
670
699
|
' - 在 Codex 中 propose/quick/context 会按 CODEX_THREAD_ID 占用当前 worktree;其他任务必须切换到独立 worktree。',
|
|
@@ -694,6 +723,7 @@ async function sdd(args) {
|
|
|
694
723
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
695
724
|
const result = proposeSddChange({
|
|
696
725
|
cwd: process.cwd(),
|
|
726
|
+
configText: readWorkspaceConfigText(),
|
|
697
727
|
changeId,
|
|
698
728
|
title: flags.title || flags.name,
|
|
699
729
|
riskLevel: flags.risk || flags['risk-level'],
|
|
@@ -717,6 +747,7 @@ async function sdd(args) {
|
|
|
717
747
|
: {};
|
|
718
748
|
const result = createQuickSddChange({
|
|
719
749
|
cwd: process.cwd(),
|
|
750
|
+
configText: readWorkspaceConfigText(),
|
|
720
751
|
changeId,
|
|
721
752
|
title: flags.title || flags.name,
|
|
722
753
|
changeKind: flags.kind || flags['change-kind'],
|
|
@@ -734,12 +765,21 @@ async function sdd(args) {
|
|
|
734
765
|
if (subcommand === 'bundle') {
|
|
735
766
|
const changeId = positional[0] || flags.change;
|
|
736
767
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
768
|
+
const sourceChangeIds = String(flags.changes || '')
|
|
769
|
+
.split(',')
|
|
770
|
+
.map(item => item.trim())
|
|
771
|
+
.filter(Boolean);
|
|
772
|
+
const integration = collectIntegrationTaskResults({
|
|
773
|
+
cwd: process.cwd(),
|
|
774
|
+
changeIds: sourceChangeIds,
|
|
775
|
+
});
|
|
737
776
|
const result = createMainlineSddBundle({
|
|
738
777
|
cwd: process.cwd(),
|
|
739
778
|
configText: readWorkspaceConfigText(),
|
|
740
779
|
changeId,
|
|
741
780
|
title: flags.title || flags.name,
|
|
742
781
|
changes: flags.changes,
|
|
782
|
+
integration,
|
|
743
783
|
profile: flags.profile,
|
|
744
784
|
force: Boolean(flags.force),
|
|
745
785
|
});
|
|
@@ -750,11 +790,47 @@ async function sdd(args) {
|
|
|
750
790
|
return;
|
|
751
791
|
}
|
|
752
792
|
|
|
793
|
+
if (subcommand === 'ready') {
|
|
794
|
+
const changeId = positional[0] || flags.change;
|
|
795
|
+
const scope = getSddChangeScope({
|
|
796
|
+
cwd: process.cwd(),
|
|
797
|
+
configText: readWorkspaceConfigText(),
|
|
798
|
+
changeId,
|
|
799
|
+
});
|
|
800
|
+
if (scope.change?.status !== 'approved' || !scope.change?.approvedAt) {
|
|
801
|
+
fail(`INTEGRATION_TASK_CHANGE_NOT_APPROVED: ${changeId} 尚未 approved`);
|
|
802
|
+
}
|
|
803
|
+
const result = recordIntegrationTaskResult({
|
|
804
|
+
cwd: process.cwd(),
|
|
805
|
+
changeId,
|
|
806
|
+
targets: scope.targets,
|
|
807
|
+
changedFiles: scope.changedFiles,
|
|
808
|
+
});
|
|
809
|
+
const ownerStatus = getWorktreeOwnerStatus({ cwd: process.cwd() });
|
|
810
|
+
if (
|
|
811
|
+
ownerStatus.ownedByCurrentThread &&
|
|
812
|
+
ownerStatus.owner?.changeId === changeId
|
|
813
|
+
) {
|
|
814
|
+
const released = releaseWorktreeOwner({
|
|
815
|
+
cwd: process.cwd(),
|
|
816
|
+
expectedThreadId: ownerStatus.owner.threadId,
|
|
817
|
+
expectedChangeId: changeId,
|
|
818
|
+
});
|
|
819
|
+
result.worktreeOwnershipReleased = Boolean(released.released);
|
|
820
|
+
}
|
|
821
|
+
if (flags.json) return writeJson(result);
|
|
822
|
+
print(
|
|
823
|
+
`任务已准备合并: ${result.changeId}@${result.commit.slice(0, 12)}\nbranch=${result.branch}${result.worktreeOwnershipReleased ? '\nworktree owner 已自动释放' : ''}`
|
|
824
|
+
);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
753
828
|
if (subcommand === 'approve') {
|
|
754
829
|
const changeId = positional[0] || flags.change;
|
|
755
830
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
756
831
|
const result = approveSddChange({
|
|
757
832
|
cwd: process.cwd(),
|
|
833
|
+
configText: readWorkspaceConfigText(),
|
|
758
834
|
changeId,
|
|
759
835
|
summary: flags.summary || flags.note,
|
|
760
836
|
});
|
|
@@ -806,7 +882,6 @@ async function sdd(args) {
|
|
|
806
882
|
|
|
807
883
|
if (subcommand === 'verify') {
|
|
808
884
|
const changeId = positional[0] || flags.change;
|
|
809
|
-
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
810
885
|
const hasExplicitReleaseScope = Boolean(
|
|
811
886
|
changeId || flags.changed || flags['changed-only'] || flags.since || flags.all || flags['no-changed']
|
|
812
887
|
);
|
|
@@ -821,9 +896,10 @@ async function sdd(args) {
|
|
|
821
896
|
: null;
|
|
822
897
|
const result = verifySddChange({
|
|
823
898
|
cwd: process.cwd(),
|
|
899
|
+
configText: readWorkspaceConfigText(),
|
|
824
900
|
changeId,
|
|
825
901
|
releasePlan,
|
|
826
|
-
writeMetadata:
|
|
902
|
+
writeMetadata: false,
|
|
827
903
|
stage: flags.stage,
|
|
828
904
|
});
|
|
829
905
|
if (flags.json) {
|
|
@@ -836,10 +912,32 @@ async function sdd(args) {
|
|
|
836
912
|
return;
|
|
837
913
|
}
|
|
838
914
|
|
|
915
|
+
if (subcommand === 'render') {
|
|
916
|
+
const changeId = positional[0] || flags.change;
|
|
917
|
+
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
918
|
+
const result = renderSddDocumentation({
|
|
919
|
+
cwd: process.cwd(),
|
|
920
|
+
configText: readWorkspaceConfigText(),
|
|
921
|
+
changeId,
|
|
922
|
+
force: Boolean(flags.force),
|
|
923
|
+
});
|
|
924
|
+
if (flags.json) return writeJson(result);
|
|
925
|
+
print(
|
|
926
|
+
result.rendered.length > 0
|
|
927
|
+
? `已按需生成 SDD 文档:\n${result.rendered.map(item => `- ${item}`).join('\n')}`
|
|
928
|
+
: 'SDD 文档已存在;使用 --force 重新生成'
|
|
929
|
+
);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
839
933
|
if (subcommand === 'sync') {
|
|
840
934
|
const changeId = positional[0] || flags.change;
|
|
841
935
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
842
|
-
const result = syncSddChange({
|
|
936
|
+
const result = syncSddChange({
|
|
937
|
+
cwd: process.cwd(),
|
|
938
|
+
configText: readWorkspaceConfigText(),
|
|
939
|
+
changeId,
|
|
940
|
+
});
|
|
843
941
|
if (flags.json) return writeJson(result);
|
|
844
942
|
print(`SDD specs 已同步: ${result.change.id}\n${result.synced.map(item => `- ${item}`).join('\n')}`);
|
|
845
943
|
return;
|
|
@@ -857,6 +955,7 @@ async function sdd(args) {
|
|
|
857
955
|
});
|
|
858
956
|
const result = archiveSddChange({
|
|
859
957
|
cwd: process.cwd(),
|
|
958
|
+
configText: readWorkspaceConfigText(),
|
|
860
959
|
changeId,
|
|
861
960
|
releasePlan,
|
|
862
961
|
force: Boolean(flags.force),
|
|
@@ -867,7 +966,7 @@ async function sdd(args) {
|
|
|
867
966
|
return;
|
|
868
967
|
}
|
|
869
968
|
|
|
870
|
-
fail('用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--json]');
|
|
969
|
+
fail('用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|render|sync|archive [change] [--json]');
|
|
871
970
|
}
|
|
872
971
|
|
|
873
972
|
function parseSddAffectedFlags(flags) {
|
|
@@ -946,6 +1045,10 @@ function summarizeReleaseStepResult(result) {
|
|
|
946
1045
|
findId(value.verified) ||
|
|
947
1046
|
findId(value.prepared) ||
|
|
948
1047
|
findId(value),
|
|
1048
|
+
healthStatus:
|
|
1049
|
+
value.healthWait?.status ||
|
|
1050
|
+
value.activated?.activation?.postCommit?.status ||
|
|
1051
|
+
null,
|
|
949
1052
|
};
|
|
950
1053
|
}
|
|
951
1054
|
|
|
@@ -1023,41 +1126,40 @@ function parseChildJsonOutput(stdout) {
|
|
|
1023
1126
|
}
|
|
1024
1127
|
}
|
|
1025
1128
|
|
|
1026
|
-
function
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
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);
|
|
1129
|
+
async function runOpenXiangdaInProcess(args, options = {}) {
|
|
1130
|
+
let stdout = '';
|
|
1131
|
+
const originalWrite = process.stdout.write;
|
|
1132
|
+
const previousExitCode = process.exitCode;
|
|
1133
|
+
process.exitCode = undefined;
|
|
1134
|
+
process.stdout.write = function capture(chunk, encoding, callback) {
|
|
1135
|
+
stdout += Buffer.isBuffer(chunk) ? chunk.toString(encoding) : String(chunk);
|
|
1136
|
+
if (typeof encoding === 'function') encoding();
|
|
1137
|
+
if (typeof callback === 'function') callback();
|
|
1138
|
+
return true;
|
|
1139
|
+
};
|
|
1140
|
+
try {
|
|
1141
|
+
await mainImpl([...args, '--json']);
|
|
1142
|
+
if (process.exitCode && process.exitCode !== 0) {
|
|
1143
|
+
const error = new Error(
|
|
1144
|
+
maskText(stdout).trim() ||
|
|
1145
|
+
`openxiangda ${args.join(' ')} exited with ${process.exitCode}`
|
|
1146
|
+
);
|
|
1054
1147
|
error.code =
|
|
1055
|
-
message.match(/\b([A-Z][A-Z0-9_]{3,})\b/)?.[1] ||
|
|
1148
|
+
error.message.match(/\b([A-Z][A-Z0-9_]{3,})\b/)?.[1] ||
|
|
1056
1149
|
'RELEASE_STEP_FAILED';
|
|
1057
|
-
error.exitCode =
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1150
|
+
error.exitCode = process.exitCode;
|
|
1151
|
+
throw error;
|
|
1152
|
+
}
|
|
1153
|
+
return parseChildJsonOutput(stdout);
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
if (!options.quiet && stdout.trim()) {
|
|
1156
|
+
warn(maskText(stdout).trimEnd());
|
|
1157
|
+
}
|
|
1158
|
+
throw error;
|
|
1159
|
+
} finally {
|
|
1160
|
+
process.stdout.write = originalWrite;
|
|
1161
|
+
process.exitCode = previousExitCode;
|
|
1162
|
+
}
|
|
1061
1163
|
}
|
|
1062
1164
|
|
|
1063
1165
|
function normalizeReleaseWaitSeconds(flags) {
|
|
@@ -1273,6 +1375,19 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1273
1375
|
target.profileName,
|
|
1274
1376
|
changeId
|
|
1275
1377
|
);
|
|
1378
|
+
const releaseScope = getSddChangeScope({
|
|
1379
|
+
cwd: process.cwd(),
|
|
1380
|
+
configText: readWorkspaceConfigText(),
|
|
1381
|
+
changeId,
|
|
1382
|
+
});
|
|
1383
|
+
const isMainlineBundle =
|
|
1384
|
+
releaseScope.change?.changeMode === 'mainline-bundle';
|
|
1385
|
+
const integration = isMainlineBundle
|
|
1386
|
+
? inspectIntegrationBundleCommits(
|
|
1387
|
+
releaseScope.release?.integration,
|
|
1388
|
+
{ cwd: process.cwd() }
|
|
1389
|
+
)
|
|
1390
|
+
: null;
|
|
1276
1391
|
const planHash = releasePlanHash({
|
|
1277
1392
|
schemaVersion: 'openxiangda_release_execution_v1',
|
|
1278
1393
|
appType: target.appType,
|
|
@@ -1292,8 +1407,14 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1292
1407
|
targets: plan.targets,
|
|
1293
1408
|
activationTargets: plan.activationTargets,
|
|
1294
1409
|
steps,
|
|
1410
|
+
integration,
|
|
1295
1411
|
};
|
|
1296
1412
|
}
|
|
1413
|
+
if (isMainlineBundle) {
|
|
1414
|
+
assertIntegrationBundleCommits(releaseScope.release?.integration, {
|
|
1415
|
+
cwd: process.cwd(),
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1297
1418
|
const verification = verifySddChange({
|
|
1298
1419
|
cwd: process.cwd(),
|
|
1299
1420
|
changeId,
|
|
@@ -1301,7 +1422,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1301
1422
|
...plan,
|
|
1302
1423
|
commands: steps.map(step => step.command),
|
|
1303
1424
|
},
|
|
1304
|
-
writeMetadata:
|
|
1425
|
+
writeMetadata: false,
|
|
1305
1426
|
stage: 'prepublish',
|
|
1306
1427
|
});
|
|
1307
1428
|
if (!verification.passed) {
|
|
@@ -1428,7 +1549,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1428
1549
|
execution.updatedAt = journalStep.startedAt;
|
|
1429
1550
|
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1430
1551
|
warn(`发布步骤开始: ${step.id}`);
|
|
1431
|
-
const result = await
|
|
1552
|
+
const result = await runOpenXiangdaInProcess(step.args);
|
|
1432
1553
|
journalStep.status = 'completed';
|
|
1433
1554
|
journalStep.completedAt = new Date().toISOString();
|
|
1434
1555
|
journalStep.result = summarizeReleaseStepResult(result);
|
|
@@ -1497,7 +1618,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1497
1618
|
writePrivateJsonAtomic(releaseExecutionFile(changeId), execution);
|
|
1498
1619
|
if (!execution.writeAttempted) {
|
|
1499
1620
|
try {
|
|
1500
|
-
await
|
|
1621
|
+
await runOpenXiangdaInProcess(
|
|
1501
1622
|
[
|
|
1502
1623
|
'release',
|
|
1503
1624
|
'end',
|
|
@@ -1521,9 +1642,9 @@ async function release(args) {
|
|
|
1521
1642
|
const { flags, positional } = parseArgs(rest);
|
|
1522
1643
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1523
1644
|
print([
|
|
1524
|
-
'用法: openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
1645
|
+
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
1525
1646
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
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]',
|
|
1647
|
+
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
|
|
1527
1648
|
'常用流程:',
|
|
1528
1649
|
' openxiangda release publish --change <id> --profile <name>',
|
|
1529
1650
|
' openxiangda release begin --change <id> --profile <name>',
|
|
@@ -1588,6 +1709,28 @@ async function release(args) {
|
|
|
1588
1709
|
}
|
|
1589
1710
|
const leaseIdFromArgs = positional[0] || readStringFlag(flags, 'lease-id');
|
|
1590
1711
|
|
|
1712
|
+
if (subcommand === 'explain') {
|
|
1713
|
+
const changeId = positional[0] || readStringFlag(flags, 'change');
|
|
1714
|
+
if (!changeId) {
|
|
1715
|
+
fail('用法: openxiangda release explain --change <id> [--json]');
|
|
1716
|
+
}
|
|
1717
|
+
const execution = readReleaseExecution(changeId);
|
|
1718
|
+
const result = {
|
|
1719
|
+
changeId,
|
|
1720
|
+
execution,
|
|
1721
|
+
diagnosis: explainReleaseExecution(execution),
|
|
1722
|
+
};
|
|
1723
|
+
if (flags.json) return writeJson(result);
|
|
1724
|
+
print(
|
|
1725
|
+
[
|
|
1726
|
+
`release ${changeId}: ${result.diagnosis.state}`,
|
|
1727
|
+
result.diagnosis.summary,
|
|
1728
|
+
`下一步: ${result.diagnosis.nextAction}`,
|
|
1729
|
+
].join('\n')
|
|
1730
|
+
);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1591
1734
|
if (subcommand === 'status') {
|
|
1592
1735
|
const local = getStoredPublishLease(target);
|
|
1593
1736
|
const baseline = getStoredChangeBaseline(target, {
|
|
@@ -1604,16 +1747,28 @@ async function release(args) {
|
|
|
1604
1747
|
target,
|
|
1605
1748
|
clientSessionId
|
|
1606
1749
|
);
|
|
1750
|
+
const changeId =
|
|
1751
|
+
positional[0] ||
|
|
1752
|
+
readStringFlag(flags, 'change') ||
|
|
1753
|
+
local?.changeId ||
|
|
1754
|
+
baseline?.changeId;
|
|
1755
|
+
const execution = changeId ? readReleaseExecution(changeId) : null;
|
|
1607
1756
|
const result = {
|
|
1608
1757
|
remote,
|
|
1609
1758
|
local,
|
|
1610
1759
|
releaseSourceRevision: baseline?.releaseSourceRevision || null,
|
|
1611
1760
|
integration: null,
|
|
1612
1761
|
integrationCheckRequired: Boolean(baseline?.releaseSourceRevision),
|
|
1762
|
+
changeId: changeId || null,
|
|
1763
|
+
execution,
|
|
1764
|
+
diagnosis: explainReleaseExecution(execution),
|
|
1613
1765
|
localOwnedByCurrentSession: local
|
|
1614
1766
|
? isPublishLeaseOwnedByCurrentSession(local)
|
|
1615
1767
|
: null,
|
|
1616
1768
|
};
|
|
1769
|
+
if (flags.watch) {
|
|
1770
|
+
return await watchTaskStatus(config, target, changeId, flags);
|
|
1771
|
+
}
|
|
1617
1772
|
if (flags.json) return writeJson(result);
|
|
1618
1773
|
printPublishLeaseStatus(result);
|
|
1619
1774
|
return;
|
|
@@ -1623,10 +1778,14 @@ async function release(args) {
|
|
|
1623
1778
|
const baseline = getStoredChangeBaseline(target, {
|
|
1624
1779
|
access: 'reconciliation-read',
|
|
1625
1780
|
});
|
|
1626
|
-
const
|
|
1781
|
+
const changeId = positional[0] || readStringFlag(flags, 'change');
|
|
1782
|
+
const recovered = readReleaseRecoveryContext(target, changeId);
|
|
1783
|
+
const sourceRevision =
|
|
1784
|
+
baseline?.releaseSourceRevision ||
|
|
1785
|
+
recovered?.context?.releaseSourceRevision;
|
|
1627
1786
|
if (!sourceRevision) {
|
|
1628
1787
|
fail(
|
|
1629
|
-
'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision
|
|
1788
|
+
'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision;发布已结束时请同时传入 --change <id>,以便从私有执行日志恢复主线证据'
|
|
1630
1789
|
);
|
|
1631
1790
|
}
|
|
1632
1791
|
const result = inspectReleaseSourceIntegration(
|
|
@@ -1868,6 +2027,233 @@ async function release(args) {
|
|
|
1868
2027
|
fail('用法: openxiangda release publish|begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
|
|
1869
2028
|
}
|
|
1870
2029
|
|
|
2030
|
+
async function task(args) {
|
|
2031
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
2032
|
+
const { flags, positional } = parseArgs(rest);
|
|
2033
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2034
|
+
print([
|
|
2035
|
+
'用法: openxiangda task status --change <id> [--profile name] [--watch] [--interval-seconds 5] [--timeout-seconds 300] [--json]',
|
|
2036
|
+
'说明:',
|
|
2037
|
+
' - 聚合 SDD、task result、主线包含关系、本地 execution、远端 lease 和 App Release post-commit。',
|
|
2038
|
+
' - --watch 只轮询只读状态,不会续租、重试或写平台。',
|
|
2039
|
+
].join('\n'));
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (subcommand !== 'status') {
|
|
2043
|
+
fail('用法: openxiangda task status --change <id> [--profile name] [--watch]');
|
|
2044
|
+
}
|
|
2045
|
+
const changeId = positional[0] || readStringFlag(flags, 'change');
|
|
2046
|
+
if (!changeId) {
|
|
2047
|
+
fail('用法: openxiangda task status --change <id> [--profile name]');
|
|
2048
|
+
}
|
|
2049
|
+
const config = loadConfig();
|
|
2050
|
+
const target = getWorkspaceTarget(
|
|
2051
|
+
config,
|
|
2052
|
+
flags.profile || config.currentProfile,
|
|
2053
|
+
flags
|
|
2054
|
+
);
|
|
2055
|
+
if (flags.watch) {
|
|
2056
|
+
return await watchTaskStatus(config, target, changeId, flags);
|
|
2057
|
+
}
|
|
2058
|
+
const result = await loadTaskStatusSnapshot(
|
|
2059
|
+
config,
|
|
2060
|
+
target,
|
|
2061
|
+
changeId
|
|
2062
|
+
);
|
|
2063
|
+
if (flags.json) return writeJson(result);
|
|
2064
|
+
printTaskStatus(result);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
async function loadTaskStatusSnapshot(config, target, changeId) {
|
|
2068
|
+
let change = null;
|
|
2069
|
+
try {
|
|
2070
|
+
const scope = getSddChangeScope({
|
|
2071
|
+
cwd: process.cwd(),
|
|
2072
|
+
configText: readWorkspaceConfigText(),
|
|
2073
|
+
changeId,
|
|
2074
|
+
});
|
|
2075
|
+
change = scope.change || null;
|
|
2076
|
+
} catch {
|
|
2077
|
+
// Missing local SDD is a status fact, not a status-command failure.
|
|
2078
|
+
}
|
|
2079
|
+
const taskResult = readIntegrationTaskResult(
|
|
2080
|
+
process.cwd(),
|
|
2081
|
+
changeId
|
|
2082
|
+
);
|
|
2083
|
+
const integration = taskResult
|
|
2084
|
+
? inspectIntegrationBundleCommits(
|
|
2085
|
+
{
|
|
2086
|
+
requiredCommits: [
|
|
2087
|
+
{
|
|
2088
|
+
changeId,
|
|
2089
|
+
commit: taskResult.commit,
|
|
2090
|
+
treeHash: taskResult.treeHash,
|
|
2091
|
+
sourceBranch: taskResult.branch,
|
|
2092
|
+
},
|
|
2093
|
+
],
|
|
2094
|
+
},
|
|
2095
|
+
{ cwd: process.cwd() }
|
|
2096
|
+
)
|
|
2097
|
+
: null;
|
|
2098
|
+
const execution = readReleaseExecution(changeId);
|
|
2099
|
+
const localLease = getStoredPublishLease(target);
|
|
2100
|
+
const baseline = getStoredChangeBaseline(target, {
|
|
2101
|
+
access: 'reconciliation-read',
|
|
2102
|
+
});
|
|
2103
|
+
const clientSessionId =
|
|
2104
|
+
localLease?.clientSessionId ||
|
|
2105
|
+
baseline?.clientSessionId ||
|
|
2106
|
+
createPublishClientSessionId();
|
|
2107
|
+
let remoteLease = null;
|
|
2108
|
+
let remoteLeaseError = null;
|
|
2109
|
+
try {
|
|
2110
|
+
remoteLease = await fetchPublishLeaseStatus(
|
|
2111
|
+
config,
|
|
2112
|
+
target,
|
|
2113
|
+
clientSessionId
|
|
2114
|
+
);
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
remoteLeaseError = {
|
|
2117
|
+
code: error?.code || 'LEASE_STATUS_UNAVAILABLE',
|
|
2118
|
+
message: maskText(error?.message || String(error)),
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
const appReleaseId = findAppReleaseId(execution?.steps || []);
|
|
2122
|
+
let postCommit = null;
|
|
2123
|
+
let postCommitError = null;
|
|
2124
|
+
if (appReleaseId) {
|
|
2125
|
+
try {
|
|
2126
|
+
postCommit = await requestWithAuth(
|
|
2127
|
+
config,
|
|
2128
|
+
target.profileName,
|
|
2129
|
+
appReleaseApiPath(
|
|
2130
|
+
target,
|
|
2131
|
+
`/${encodeURIComponent(appReleaseId)}/post-commit`
|
|
2132
|
+
)
|
|
2133
|
+
);
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
postCommitError = {
|
|
2136
|
+
code: error?.code || 'POST_COMMIT_STATUS_UNAVAILABLE',
|
|
2137
|
+
message: maskText(error?.message || String(error)),
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
...buildTaskStatus({
|
|
2143
|
+
changeId,
|
|
2144
|
+
change,
|
|
2145
|
+
taskResult,
|
|
2146
|
+
integration,
|
|
2147
|
+
execution,
|
|
2148
|
+
remoteLease,
|
|
2149
|
+
postCommit,
|
|
2150
|
+
}),
|
|
2151
|
+
checkedAt: new Date().toISOString(),
|
|
2152
|
+
diagnostics: {
|
|
2153
|
+
remoteLeaseError,
|
|
2154
|
+
postCommitError,
|
|
2155
|
+
},
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
async function watchTaskStatus(config, target, changeId, flags = {}) {
|
|
2160
|
+
if (!changeId) {
|
|
2161
|
+
fail('task/release status --watch 需要 --change <id>');
|
|
2162
|
+
}
|
|
2163
|
+
const intervalSeconds = normalizeWatchNumber(
|
|
2164
|
+
flags['interval-seconds'],
|
|
2165
|
+
5,
|
|
2166
|
+
1,
|
|
2167
|
+
60,
|
|
2168
|
+
'--interval-seconds'
|
|
2169
|
+
);
|
|
2170
|
+
const timeoutSeconds = normalizeWatchNumber(
|
|
2171
|
+
flags['timeout-seconds'],
|
|
2172
|
+
300,
|
|
2173
|
+
1,
|
|
2174
|
+
3600,
|
|
2175
|
+
'--timeout-seconds'
|
|
2176
|
+
);
|
|
2177
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
2178
|
+
let previousFingerprint = '';
|
|
2179
|
+
let result;
|
|
2180
|
+
for (;;) {
|
|
2181
|
+
result = await loadTaskStatusSnapshot(
|
|
2182
|
+
config,
|
|
2183
|
+
target,
|
|
2184
|
+
changeId
|
|
2185
|
+
);
|
|
2186
|
+
const fingerprint = [
|
|
2187
|
+
result.phase,
|
|
2188
|
+
result.progress.currentStep,
|
|
2189
|
+
result.progress.completed,
|
|
2190
|
+
result.release.postCommitStatus,
|
|
2191
|
+
result.blocker?.layer,
|
|
2192
|
+
].join(':');
|
|
2193
|
+
if (!flags.json && fingerprint !== previousFingerprint) {
|
|
2194
|
+
printTaskStatus(result);
|
|
2195
|
+
}
|
|
2196
|
+
previousFingerprint = fingerprint;
|
|
2197
|
+
if (
|
|
2198
|
+
['healthy', 'failed', 'write-review', 'integration-ready'].includes(
|
|
2199
|
+
result.phase
|
|
2200
|
+
)
|
|
2201
|
+
) {
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
if (Date.now() >= deadline) {
|
|
2205
|
+
result.timedOut = true;
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
await sleep(
|
|
2209
|
+
Math.min(intervalSeconds * 1000, Math.max(1, deadline - Date.now()))
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
if (flags.json) return writeJson(result);
|
|
2213
|
+
if (result.timedOut) {
|
|
2214
|
+
warn(
|
|
2215
|
+
`状态观察已达到 ${timeoutSeconds}s;没有执行任何写入,稍后可继续使用同一命令。`
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
return result;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
function normalizeWatchNumber(value, fallback, minimum, maximum, label) {
|
|
2222
|
+
const number =
|
|
2223
|
+
value === undefined || value === null || value === ''
|
|
2224
|
+
? fallback
|
|
2225
|
+
: Number(value);
|
|
2226
|
+
if (
|
|
2227
|
+
!Number.isSafeInteger(number) ||
|
|
2228
|
+
number < minimum ||
|
|
2229
|
+
number > maximum
|
|
2230
|
+
) {
|
|
2231
|
+
fail(`${label} 必须是 ${minimum}-${maximum} 之间的整数`);
|
|
2232
|
+
}
|
|
2233
|
+
return number;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function printTaskStatus(result) {
|
|
2237
|
+
const eta =
|
|
2238
|
+
result.estimatedRemainingMs === null
|
|
2239
|
+
? 'unknown'
|
|
2240
|
+
: formatDurationMs(result.estimatedRemainingMs);
|
|
2241
|
+
print(
|
|
2242
|
+
[
|
|
2243
|
+
`task ${result.changeId || '-'}: ${result.phase}`,
|
|
2244
|
+
`progress: ${result.progress.completed}/${result.progress.total} current=${result.progress.currentStep || '-'}`,
|
|
2245
|
+
`elapsed: ${result.elapsedMs === null ? 'unknown' : formatDurationMs(result.elapsedMs)} eta=${eta}`,
|
|
2246
|
+
`platform write: ${result.wrotePlatform ? 'yes' : 'no'} health=${result.healthy ? 'healthy' : 'pending'}`,
|
|
2247
|
+
...(result.blocker
|
|
2248
|
+
? [
|
|
2249
|
+
`blocked by: ${result.blocker.layer}${result.blocker.code ? ` (${result.blocker.code})` : ''}`,
|
|
2250
|
+
]
|
|
2251
|
+
: []),
|
|
2252
|
+
`下一步: ${result.nextAction}`,
|
|
2253
|
+
].join('\n')
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
1871
2257
|
async function fetchPublishLeaseStatus(config, target, clientSessionId) {
|
|
1872
2258
|
return await requestWithAuth(
|
|
1873
2259
|
config,
|
|
@@ -1946,6 +2332,10 @@ function backendReleaseApiPath(target, suffix = '') {
|
|
|
1946
2332
|
return `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/backend-releases${suffix}`;
|
|
1947
2333
|
}
|
|
1948
2334
|
|
|
2335
|
+
function workflowReleaseApiPath(target, suffix = '') {
|
|
2336
|
+
return `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/workflow-releases${suffix}`;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1949
2339
|
async function runBackendReleaseCommand(
|
|
1950
2340
|
config,
|
|
1951
2341
|
target,
|
|
@@ -2102,6 +2492,62 @@ function appReleaseApiPath(target, suffix = '') {
|
|
|
2102
2492
|
return `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/app-releases${suffix}`;
|
|
2103
2493
|
}
|
|
2104
2494
|
|
|
2495
|
+
async function waitForAppReleasePostCommit(
|
|
2496
|
+
config,
|
|
2497
|
+
target,
|
|
2498
|
+
releaseId,
|
|
2499
|
+
flags = {}
|
|
2500
|
+
) {
|
|
2501
|
+
const timeoutSeconds = normalizeWatchNumber(
|
|
2502
|
+
flags['health-timeout-seconds'] || flags['timeout-seconds'],
|
|
2503
|
+
60,
|
|
2504
|
+
1,
|
|
2505
|
+
900,
|
|
2506
|
+
'--health-timeout-seconds'
|
|
2507
|
+
);
|
|
2508
|
+
const intervalSeconds = normalizeWatchNumber(
|
|
2509
|
+
flags['interval-seconds'],
|
|
2510
|
+
2,
|
|
2511
|
+
1,
|
|
2512
|
+
30,
|
|
2513
|
+
'--interval-seconds'
|
|
2514
|
+
);
|
|
2515
|
+
const startedAt = Date.now();
|
|
2516
|
+
const deadline = startedAt + timeoutSeconds * 1000;
|
|
2517
|
+
let status = null;
|
|
2518
|
+
for (;;) {
|
|
2519
|
+
status = await requestWithAuth(
|
|
2520
|
+
config,
|
|
2521
|
+
target.profileName,
|
|
2522
|
+
appReleaseApiPath(
|
|
2523
|
+
target,
|
|
2524
|
+
`/${encodeURIComponent(releaseId)}/post-commit`
|
|
2525
|
+
)
|
|
2526
|
+
);
|
|
2527
|
+
if (status?.status === 'completed') {
|
|
2528
|
+
return {
|
|
2529
|
+
...status,
|
|
2530
|
+
healthy: true,
|
|
2531
|
+
waitedMs: Date.now() - startedAt,
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
if (Date.now() >= deadline) {
|
|
2535
|
+
return {
|
|
2536
|
+
...(status || {}),
|
|
2537
|
+
status: status?.status || 'pending_retry',
|
|
2538
|
+
healthy: false,
|
|
2539
|
+
timedOut: true,
|
|
2540
|
+
waitedMs: Date.now() - startedAt,
|
|
2541
|
+
message:
|
|
2542
|
+
'App Release 已原子激活,提交后动作仍由平台 outbox 自动重试。',
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
await sleep(
|
|
2546
|
+
Math.min(intervalSeconds * 1000, Math.max(1, deadline - Date.now()))
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2105
2551
|
async function fetchAppReleaseCapture(config, target, flags = {}) {
|
|
2106
2552
|
const capture = await requestWithAuth(
|
|
2107
2553
|
config,
|
|
@@ -2223,6 +2669,7 @@ const APP_RELEASE_IMMUTABLE_RESOURCE_KINDS = new Set([
|
|
|
2223
2669
|
'RuntimeRelease',
|
|
2224
2670
|
'PageRelease',
|
|
2225
2671
|
'BackendRelease',
|
|
2672
|
+
'WorkflowRelease',
|
|
2226
2673
|
'FormRelease',
|
|
2227
2674
|
]);
|
|
2228
2675
|
|
|
@@ -2233,7 +2680,7 @@ function describeAppReleaseResource(resource, index, label) {
|
|
|
2233
2680
|
const kind = String(resource.kind || '').trim();
|
|
2234
2681
|
if (!APP_RELEASE_IMMUTABLE_RESOURCE_KINDS.has(kind)) {
|
|
2235
2682
|
fail(
|
|
2236
|
-
`APP_RELEASE_STAGED_RESOURCE_KIND_UNSUPPORTED: ${label}[${index}].kind 仅支持 RuntimeRelease/PageRelease/BackendRelease/FormRelease`
|
|
2683
|
+
`APP_RELEASE_STAGED_RESOURCE_KIND_UNSUPPORTED: ${label}[${index}].kind 仅支持 RuntimeRelease/PageRelease/BackendRelease/WorkflowRelease/FormRelease`
|
|
2237
2684
|
);
|
|
2238
2685
|
}
|
|
2239
2686
|
const identity =
|
|
@@ -2578,6 +3025,26 @@ function assertStagedAppReleaseScope(target, flags, resources) {
|
|
|
2578
3025
|
);
|
|
2579
3026
|
}
|
|
2580
3027
|
|
|
3028
|
+
const expectedWorkflowSelectors = (targets.workflows || [])
|
|
3029
|
+
.map(code => `workflow:${code}`)
|
|
3030
|
+
.sort();
|
|
3031
|
+
const workflows = byKind('WorkflowRelease');
|
|
3032
|
+
if (expectedWorkflowSelectors.length > 0) {
|
|
3033
|
+
if (
|
|
3034
|
+
workflows.length !== 1 ||
|
|
3035
|
+
!exactStringSet(
|
|
3036
|
+
workflows[0]?.metadata?.handledSelectors,
|
|
3037
|
+
expectedWorkflowSelectors
|
|
3038
|
+
)
|
|
3039
|
+
) {
|
|
3040
|
+
fail(
|
|
3041
|
+
`APP_RELEASE_STAGED_SCOPE_MISMATCH: Workflow handledSelectors 必须精确为 ${expectedWorkflowSelectors.join(',')}`
|
|
3042
|
+
);
|
|
3043
|
+
}
|
|
3044
|
+
} else if (workflows.length > 0) {
|
|
3045
|
+
fail('APP_RELEASE_STAGED_SCOPE_MISMATCH: SDD 未授权 WorkflowRelease child');
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2581
3048
|
const runtimeCount = byKind('RuntimeRelease').length;
|
|
2582
3049
|
if (runtimeCount !== (targets.runtime ? 1 : 0)) {
|
|
2583
3050
|
fail(
|
|
@@ -2650,7 +3117,10 @@ function stagedAppReleaseProtocolVersion(stagedResources) {
|
|
|
2650
3117
|
''
|
|
2651
3118
|
) === APP_FUNCTION_SECRET_CAPABILITIES.backendRelease
|
|
2652
3119
|
);
|
|
2653
|
-
|
|
3120
|
+
const hasWorkflowRelease = (stagedResources || []).some(
|
|
3121
|
+
resource => resource.kind === 'WorkflowRelease'
|
|
3122
|
+
);
|
|
3123
|
+
return hasBackendReleaseV2 || hasWorkflowRelease
|
|
2654
3124
|
? APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation
|
|
2655
3125
|
: null;
|
|
2656
3126
|
}
|
|
@@ -2883,16 +3353,43 @@ async function runAppReleaseCommand(
|
|
|
2883
3353
|
data = await requestWithAuth(
|
|
2884
3354
|
config,
|
|
2885
3355
|
target.profileName,
|
|
2886
|
-
apiPathWithQuery(appReleaseApiPath(target), { limit: flags.limit })
|
|
3356
|
+
apiPathWithQuery(appReleaseApiPath(target), { limit: flags.limit })
|
|
3357
|
+
);
|
|
3358
|
+
} else if (subcommand === 'app-detail') {
|
|
3359
|
+
if (!releaseId) {
|
|
3360
|
+
fail('用法: openxiangda release app-detail <releaseId> [--profile name]');
|
|
3361
|
+
}
|
|
3362
|
+
data = await requestWithAuth(
|
|
3363
|
+
config,
|
|
3364
|
+
target.profileName,
|
|
3365
|
+
appReleaseApiPath(target, `/${encodeURIComponent(releaseId)}`)
|
|
3366
|
+
);
|
|
3367
|
+
} else if (subcommand === 'app-post-commit') {
|
|
3368
|
+
if (!releaseId) {
|
|
3369
|
+
fail(
|
|
3370
|
+
'用法: openxiangda release app-post-commit <releaseId> [--profile name]'
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
data = await requestWithAuth(
|
|
3374
|
+
config,
|
|
3375
|
+
target.profileName,
|
|
3376
|
+
appReleaseApiPath(
|
|
3377
|
+
target,
|
|
3378
|
+
`/${encodeURIComponent(releaseId)}/post-commit`
|
|
3379
|
+
)
|
|
2887
3380
|
);
|
|
2888
|
-
} else if (subcommand === 'app-
|
|
3381
|
+
} else if (subcommand === 'app-retry') {
|
|
2889
3382
|
if (!releaseId) {
|
|
2890
|
-
fail('用法: openxiangda release app-
|
|
3383
|
+
fail('用法: openxiangda release app-retry <releaseId> [--profile name]');
|
|
2891
3384
|
}
|
|
2892
3385
|
data = await requestWithAuth(
|
|
2893
3386
|
config,
|
|
2894
3387
|
target.profileName,
|
|
2895
|
-
appReleaseApiPath(
|
|
3388
|
+
appReleaseApiPath(
|
|
3389
|
+
target,
|
|
3390
|
+
`/${encodeURIComponent(releaseId)}/post-commit/retry`
|
|
3391
|
+
),
|
|
3392
|
+
{ method: 'POST', body: {} }
|
|
2896
3393
|
);
|
|
2897
3394
|
} else if (subcommand === 'app-diff') {
|
|
2898
3395
|
const fromReleaseId = releaseId || readStringFlag(flags, 'from-release-id');
|
|
@@ -3098,10 +3595,28 @@ async function runAppReleaseCommand(
|
|
|
3098
3595
|
protocolVersion,
|
|
3099
3596
|
}
|
|
3100
3597
|
);
|
|
3598
|
+
const healthWait = flags.wait
|
|
3599
|
+
? await waitForAppReleasePostCommit(
|
|
3600
|
+
config,
|
|
3601
|
+
target,
|
|
3602
|
+
activated?.id || verified?.id || prepared.id,
|
|
3603
|
+
flags
|
|
3604
|
+
)
|
|
3605
|
+
: null;
|
|
3101
3606
|
let stateReconcileWarning = null;
|
|
3102
3607
|
try {
|
|
3103
3608
|
const appHead = await loadRuntimeHeadApp(config, target);
|
|
3104
|
-
|
|
3609
|
+
const stagedRuntime = stagedResources?.find(
|
|
3610
|
+
resource => resource.kind === 'RuntimeRelease'
|
|
3611
|
+
);
|
|
3612
|
+
saveRuntimeHeadState(target, appHead, {
|
|
3613
|
+
id:
|
|
3614
|
+
stagedRuntime?.identity?.releaseId ||
|
|
3615
|
+
stagedRuntime?.releaseId ||
|
|
3616
|
+
null,
|
|
3617
|
+
buildId: stagedRuntime?.metadata?.buildId || null,
|
|
3618
|
+
assetBaseUrl: stagedRuntime?.metadata?.assetBaseUrl || null,
|
|
3619
|
+
});
|
|
3105
3620
|
} catch (error) {
|
|
3106
3621
|
stateReconcileWarning = {
|
|
3107
3622
|
component: 'RuntimeStateReadback',
|
|
@@ -3117,6 +3632,7 @@ async function runAppReleaseCommand(
|
|
|
3117
3632
|
prepared,
|
|
3118
3633
|
verified,
|
|
3119
3634
|
activated,
|
|
3635
|
+
...(healthWait ? { healthWait } : {}),
|
|
3120
3636
|
...(stateReconcileWarning ? { stateReconcileWarning } : {}),
|
|
3121
3637
|
};
|
|
3122
3638
|
} else if (subcommand === 'app-rollback') {
|
|
@@ -4092,6 +4608,47 @@ async function contract(args) {
|
|
|
4092
4608
|
fail('用法: openxiangda contract list|explain <contract-name> [--json]');
|
|
4093
4609
|
}
|
|
4094
4610
|
|
|
4611
|
+
async function policy(args) {
|
|
4612
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
4613
|
+
const { flags } = parseArgs(rest);
|
|
4614
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
4615
|
+
print('用法: openxiangda policy show|check|render [--json]');
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4618
|
+
if (subcommand === 'show') {
|
|
4619
|
+
const result = loadEngineeringPolicy();
|
|
4620
|
+
if (flags.json) return writeJson(result);
|
|
4621
|
+
print(JSON.stringify(result, null, 2));
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
4624
|
+
if (subcommand === 'check') {
|
|
4625
|
+
const result = checkEngineeringPolicyMarkers();
|
|
4626
|
+
if (!result.valid) {
|
|
4627
|
+
const error = new Error(
|
|
4628
|
+
`OPENXIANGDA_POLICY_DOC_DRIFT: ${result.files
|
|
4629
|
+
.filter(item => !item.synchronized)
|
|
4630
|
+
.map(item => item.file)
|
|
4631
|
+
.join(', ')}`
|
|
4632
|
+
);
|
|
4633
|
+
error.code = 'OPENXIANGDA_POLICY_DOC_DRIFT';
|
|
4634
|
+
throw error;
|
|
4635
|
+
}
|
|
4636
|
+
if (flags.json) return writeJson(result);
|
|
4637
|
+
print(`OpenXiangda engineering policy v${result.policyVersion} synchronized`);
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
4640
|
+
if (subcommand === 'render') {
|
|
4641
|
+
const result = {
|
|
4642
|
+
policyVersion: loadEngineeringPolicy().policyVersion,
|
|
4643
|
+
markdown: renderEngineeringPolicyMarkdown(),
|
|
4644
|
+
};
|
|
4645
|
+
if (flags.json) return writeJson(result);
|
|
4646
|
+
print(result.markdown);
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
fail('用法: openxiangda policy show|check|render [--json]');
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4095
4652
|
async function openApi(args) {
|
|
4096
4653
|
const [section, action, ...rest] = args;
|
|
4097
4654
|
const { flags, positional } = parseArgs(rest);
|
|
@@ -4263,6 +4820,29 @@ async function platform(args) {
|
|
|
4263
4820
|
}
|
|
4264
4821
|
const config = loadConfig();
|
|
4265
4822
|
|
|
4823
|
+
if (subcommand === 'prepare') {
|
|
4824
|
+
const result = prepareDependencyCapsule({
|
|
4825
|
+
cwd: process.cwd(),
|
|
4826
|
+
check: Boolean(flags.check || flags['dry-run']),
|
|
4827
|
+
force: Boolean(flags.force),
|
|
4828
|
+
quiet: Boolean(flags.json),
|
|
4829
|
+
});
|
|
4830
|
+
if (flags.json) {
|
|
4831
|
+
writeJson(result);
|
|
4832
|
+
if (!result.ready) process.exitCode = 1;
|
|
4833
|
+
return;
|
|
4834
|
+
}
|
|
4835
|
+
if (!result.ready) {
|
|
4836
|
+
fail(
|
|
4837
|
+
`依赖 capsule 尚未准备。运行: ${result.recommendedCommand}`
|
|
4838
|
+
);
|
|
4839
|
+
}
|
|
4840
|
+
print(
|
|
4841
|
+
`依赖环境已就绪: ${result.packageManager} ${result.fingerprint.slice(0, 12)} (${result.action || 'ready'})`
|
|
4842
|
+
);
|
|
4843
|
+
return;
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4266
4846
|
if (subcommand === 'add') {
|
|
4267
4847
|
const [name, rawUrl] = positional;
|
|
4268
4848
|
if (!name || !rawUrl) {
|
|
@@ -5251,7 +5831,7 @@ async function workspace(args) {
|
|
|
5251
5831
|
return;
|
|
5252
5832
|
}
|
|
5253
5833
|
|
|
5254
|
-
fail('用法: openxiangda workspace init|bind|plan|check|publish|ownership [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
|
|
5834
|
+
fail('用法: openxiangda workspace init|prepare|bind|plan|check|publish|ownership [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources]');
|
|
5255
5835
|
}
|
|
5256
5836
|
|
|
5257
5837
|
function printWorkspaceHelp() {
|
|
@@ -5261,6 +5841,7 @@ Usage:
|
|
|
5261
5841
|
openxiangda workspace init [dir] [--name package-name] [--runtime legacy|react-spa] [--install] [--force]
|
|
5262
5842
|
openxiangda workspace init [dir] --profile <name> --app-name <app-name> [--runtime legacy|react-spa] [--description text] [--install] [--force]
|
|
5263
5843
|
openxiangda workspace init [dir] --profile <name> --app-type <APP_XXX> [--runtime legacy|react-spa] [--install] [--force]
|
|
5844
|
+
openxiangda workspace prepare [--check] [--force] [--json]
|
|
5264
5845
|
openxiangda workspace bind --profile <name> --app-type <APP_XXX>
|
|
5265
5846
|
openxiangda workspace ownership status|claim|release [--change <id>] [--force --reason text]
|
|
5266
5847
|
openxiangda workspace plan [--change <id>|--changed] [--json]
|
|
@@ -5285,6 +5866,7 @@ Options:
|
|
|
5285
5866
|
--dry-run Show planned work without writing.
|
|
5286
5867
|
--resources / --skip-resources Include or skip src/resources publish.
|
|
5287
5868
|
--legacy-form-bundle Force legacy workspace publish in React SPA workspaces.
|
|
5869
|
+
prepare Reuse one lockfile-addressed dependency capsule across worktrees.
|
|
5288
5870
|
plan/check Show release order and fail on risky AI fallback patterns.`);
|
|
5289
5871
|
}
|
|
5290
5872
|
|
|
@@ -9531,14 +10113,14 @@ async function resource(args) {
|
|
|
9531
10113
|
' openxiangda resource validate --profile <name>',
|
|
9532
10114
|
' openxiangda resource plan [type] --only <code1,code2> --profile <name> --json',
|
|
9533
10115
|
' openxiangda resource publish [type] --only <code1,code2> --profile <name>',
|
|
9534
|
-
' openxiangda resource publish function --only
|
|
10116
|
+
' openxiangda resource publish function,workflow --only function:<code>,workflow:<code> --stage-only --profile <name>',
|
|
9535
10117
|
' openxiangda resource publish form-settings --only <code> --activate --profile <name>',
|
|
9536
10118
|
' openxiangda resource publish [type] --all --reason "approved app-wide closure" --profile <name>',
|
|
9537
10119
|
'说明:',
|
|
9538
10120
|
' - 读取 src/resources/**;workflowFile 会先走非写入编译校验。',
|
|
9539
10121
|
' - publish 默认跳过 plan 判定为 noop 的资源,避免无意义 upsert;需要强制写入时加 --force。',
|
|
9540
10122
|
' - Form settings bundle 默认只 stage,不推进线上 Form head;只有显式 --activate 才 CAS 激活。',
|
|
9541
|
-
' - Function/Automation 可用 --stage-only 只 prepare + verify
|
|
10123
|
+
' - Function/Automation/Workflow 可用 --stage-only 只 prepare + verify immutable child;随后由 release app-finalize 一次原子激活。',
|
|
9542
10124
|
' - publish --dry-run 等价于发布前计划,不会写平台资源。',
|
|
9543
10125
|
' - 在线 publish 必须用 --only/--code 精确选择;真正全量发布必须显式 --all --reason,SDD bypass 不能绕过此范围门。',
|
|
9544
10126
|
' - 历史代码已先合入主线、线上又来自多次分批发布时,可显式使用 --adopt-online-baseline --adoption-reason "...";仅允许绑定同一 change/lease 的精确非删除范围,仍保留 lease、冻结 head 检查与写入 CAS。',
|
|
@@ -9743,7 +10325,15 @@ async function resource(args) {
|
|
|
9743
10325
|
baseRef: flags.since || 'HEAD',
|
|
9744
10326
|
replaceManifest: Boolean(flags['replace-manifest']),
|
|
9745
10327
|
activateFormBundles: Boolean(flags.activate),
|
|
9746
|
-
stageBackendReleaseOnly:
|
|
10328
|
+
stageBackendReleaseOnly:
|
|
10329
|
+
Boolean(flags['stage-only']) &&
|
|
10330
|
+
Boolean(
|
|
10331
|
+
typeFilters?.has('functions') ||
|
|
10332
|
+
typeFilters?.has('automations')
|
|
10333
|
+
),
|
|
10334
|
+
stageWorkflowReleaseOnly:
|
|
10335
|
+
Boolean(flags['stage-only']) &&
|
|
10336
|
+
Boolean(typeFilters?.has('workflows')),
|
|
9747
10337
|
})
|
|
9748
10338
|
);
|
|
9749
10339
|
if (changeBaselinePreflight) {
|
|
@@ -9795,9 +10385,11 @@ function assertBackendStageOnlyScope(subcommand, flags, typeFilters) {
|
|
|
9795
10385
|
const normalized = Array.from(typeFilters || []);
|
|
9796
10386
|
if (
|
|
9797
10387
|
normalized.length === 0 ||
|
|
9798
|
-
normalized.some(
|
|
10388
|
+
normalized.some(
|
|
10389
|
+
type => !['functions', 'automations', 'workflows'].includes(type)
|
|
10390
|
+
)
|
|
9799
10391
|
) {
|
|
9800
|
-
fail('--stage-only 必须显式选择 function 或
|
|
10392
|
+
fail('--stage-only 必须显式选择 function、automation 或 workflow 资源类型');
|
|
9801
10393
|
}
|
|
9802
10394
|
}
|
|
9803
10395
|
|
|
@@ -9953,11 +10545,11 @@ async function runtime(args) {
|
|
|
9953
10545
|
const { flags, positional } = parseArgs(rest);
|
|
9954
10546
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
9955
10547
|
print([
|
|
9956
|
-
'用法: openxiangda runtime deploy|releases|activate [--profile name] [--json]',
|
|
10548
|
+
'用法: openxiangda runtime deploy|releases [--full]|activate [--profile name] [--json]',
|
|
9957
10549
|
'常用流程:',
|
|
9958
10550
|
' npm run build',
|
|
9959
10551
|
' openxiangda runtime deploy --no-activate --change <change> --profile <name> --json',
|
|
9960
|
-
' openxiangda runtime releases --profile <name>',
|
|
10552
|
+
' openxiangda runtime releases --profile <name> [--full]',
|
|
9961
10553
|
'说明:',
|
|
9962
10554
|
' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
|
|
9963
10555
|
' - 默认 --upload-mode auto;multipart 上传遇到 403 时自动切换到平台内置对象存储直传。',
|
|
@@ -9984,8 +10576,9 @@ async function runtime(args) {
|
|
|
9984
10576
|
target.profileName,
|
|
9985
10577
|
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/runtime/releases`
|
|
9986
10578
|
);
|
|
9987
|
-
|
|
9988
|
-
|
|
10579
|
+
const output = flags.full ? data : summarizeRuntimeReleaseList(data);
|
|
10580
|
+
if (flags.json) return writeJson(output);
|
|
10581
|
+
print(JSON.stringify(output, null, 2));
|
|
9989
10582
|
return;
|
|
9990
10583
|
}
|
|
9991
10584
|
|
|
@@ -10298,6 +10891,7 @@ async function runtime(args) {
|
|
|
10298
10891
|
const runtimeContentHash = String(data?.contentHash || '')
|
|
10299
10892
|
.trim()
|
|
10300
10893
|
.toLowerCase();
|
|
10894
|
+
const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
|
|
10301
10895
|
const stagedResource = flags['no-activate']
|
|
10302
10896
|
? stripUndefinedValues({
|
|
10303
10897
|
kind: 'RuntimeRelease',
|
|
@@ -10311,6 +10905,7 @@ async function runtime(args) {
|
|
|
10311
10905
|
metadata: {
|
|
10312
10906
|
buildId: data?.buildId || buildId,
|
|
10313
10907
|
releaseStatus: data?.status || 'staged',
|
|
10908
|
+
assetBaseUrl: releaseAssetBaseUrl,
|
|
10314
10909
|
},
|
|
10315
10910
|
})
|
|
10316
10911
|
: null;
|
|
@@ -10335,7 +10930,6 @@ async function runtime(args) {
|
|
|
10335
10930
|
target
|
|
10336
10931
|
)
|
|
10337
10932
|
: null;
|
|
10338
|
-
const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
|
|
10339
10933
|
const result = {
|
|
10340
10934
|
appType: target.appType,
|
|
10341
10935
|
buildId,
|
|
@@ -10381,7 +10975,7 @@ async function runtime(args) {
|
|
|
10381
10975
|
}
|
|
10382
10976
|
}
|
|
10383
10977
|
|
|
10384
|
-
fail('用法: openxiangda runtime deploy|releases|activate [--profile name]');
|
|
10978
|
+
fail('用法: openxiangda runtime deploy|releases [--full]|activate [--profile name]');
|
|
10385
10979
|
}
|
|
10386
10980
|
|
|
10387
10981
|
async function loadRuntimeHeadApp(config, target) {
|
|
@@ -10568,9 +11162,11 @@ async function uploadRuntimeDistFilesStaged(options) {
|
|
|
10568
11162
|
async file => {
|
|
10569
11163
|
const uploaded = await uploadRuntimeDistFile(options, file);
|
|
10570
11164
|
completed += 1;
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
11165
|
+
if (shouldReportRuntimeUploadProgress(completed, files.length)) {
|
|
11166
|
+
printRuntimeProgress(
|
|
11167
|
+
`runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
|
|
11168
|
+
);
|
|
11169
|
+
}
|
|
10574
11170
|
return uploaded;
|
|
10575
11171
|
}
|
|
10576
11172
|
);
|
|
@@ -10625,9 +11221,11 @@ async function uploadRuntimeDistFilesOssDirect(options) {
|
|
|
10625
11221
|
);
|
|
10626
11222
|
const uploaded = await uploadRuntimeDistFileToSignedUrl(options, file, uploadInfo);
|
|
10627
11223
|
completed += 1;
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
11224
|
+
if (shouldReportRuntimeUploadProgress(completed, files.length)) {
|
|
11225
|
+
printRuntimeProgress(
|
|
11226
|
+
`runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
|
|
11227
|
+
);
|
|
11228
|
+
}
|
|
10631
11229
|
return { ...uploaded, storageProvider };
|
|
10632
11230
|
}
|
|
10633
11231
|
);
|
|
@@ -10889,21 +11487,71 @@ function saveRuntimeReleaseState(target, release) {
|
|
|
10889
11487
|
saveProjectState(target.state);
|
|
10890
11488
|
}
|
|
10891
11489
|
|
|
10892
|
-
function saveRuntimeHeadState(target, app) {
|
|
11490
|
+
function saveRuntimeHeadState(target, app, fallbackRelease = {}) {
|
|
10893
11491
|
const activeReleaseId =
|
|
10894
|
-
app?.activeRuntimeReleaseId ||
|
|
11492
|
+
app?.activeRuntimeReleaseId ||
|
|
11493
|
+
app?.activeRuntime?.releaseId ||
|
|
11494
|
+
fallbackRelease?.id ||
|
|
11495
|
+
fallbackRelease?.releaseId ||
|
|
11496
|
+
null;
|
|
10895
11497
|
const activeBuildId =
|
|
10896
|
-
app?.activeRuntimeBuildId ||
|
|
11498
|
+
app?.activeRuntimeBuildId ||
|
|
11499
|
+
app?.activeRuntime?.buildId ||
|
|
11500
|
+
fallbackRelease?.buildId ||
|
|
11501
|
+
null;
|
|
10897
11502
|
if (!activeReleaseId && !activeBuildId) return false;
|
|
10898
11503
|
saveRuntimeReleaseState(target, {
|
|
10899
11504
|
id: activeReleaseId,
|
|
10900
11505
|
buildId: activeBuildId,
|
|
10901
11506
|
assetBaseUrl:
|
|
10902
|
-
app?.runtimeAssetBaseUrl ||
|
|
11507
|
+
app?.runtimeAssetBaseUrl ||
|
|
11508
|
+
app?.activeRuntime?.assetBaseUrl ||
|
|
11509
|
+
fallbackRelease?.assetBaseUrl ||
|
|
11510
|
+
null,
|
|
10903
11511
|
});
|
|
10904
11512
|
return true;
|
|
10905
11513
|
}
|
|
10906
11514
|
|
|
11515
|
+
function summarizeRuntimeReleaseList(data) {
|
|
11516
|
+
const items = Array.isArray(data)
|
|
11517
|
+
? data
|
|
11518
|
+
: Array.isArray(data?.items)
|
|
11519
|
+
? data.items
|
|
11520
|
+
: [];
|
|
11521
|
+
const summarized = items.map(item =>
|
|
11522
|
+
stripUndefinedValues({
|
|
11523
|
+
id: item?.id,
|
|
11524
|
+
version: item?.version,
|
|
11525
|
+
buildId: item?.buildId,
|
|
11526
|
+
status: item?.status,
|
|
11527
|
+
storageProvider: item?.storageProvider,
|
|
11528
|
+
fileCount: item?.fileCount,
|
|
11529
|
+
sizeBytes: item?.sizeBytes,
|
|
11530
|
+
contentHash: item?.contentHash,
|
|
11531
|
+
sourceRevision: item?.sourceRevision,
|
|
11532
|
+
parentReleaseId: item?.parentReleaseId,
|
|
11533
|
+
parentSourceRevision: item?.parentSourceRevision,
|
|
11534
|
+
lineageBypassReason: item?.lineageBypassReason,
|
|
11535
|
+
lineageBootstrap: item?.lineageBootstrap,
|
|
11536
|
+
releaseNotes: item?.releaseNotes,
|
|
11537
|
+
createdAt: item?.createdAt,
|
|
11538
|
+
activatedAt: item?.activatedAt,
|
|
11539
|
+
})
|
|
11540
|
+
);
|
|
11541
|
+
return Array.isArray(data)
|
|
11542
|
+
? summarized
|
|
11543
|
+
: {
|
|
11544
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
11545
|
+
items: summarized,
|
|
11546
|
+
};
|
|
11547
|
+
}
|
|
11548
|
+
|
|
11549
|
+
function shouldReportRuntimeUploadProgress(completed, total) {
|
|
11550
|
+
if (total <= 10) return true;
|
|
11551
|
+
const interval = Math.max(1, Math.ceil(total / 10));
|
|
11552
|
+
return completed === 1 || completed === total || completed % interval === 0;
|
|
11553
|
+
}
|
|
11554
|
+
|
|
10907
11555
|
function formatBytes(value) {
|
|
10908
11556
|
const bytes = Number(value || 0);
|
|
10909
11557
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -14350,7 +14998,7 @@ async function tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
|
|
|
14350
14998
|
|
|
14351
14999
|
let usedLegacyBuilder = false;
|
|
14352
15000
|
for (const sourceInfo of sourceInfos) {
|
|
14353
|
-
const result =
|
|
15001
|
+
const result = runLegacyWorkspaceJsCodeBuild(
|
|
14354
15002
|
workspaceRoot,
|
|
14355
15003
|
sourceInfo.scriptCode,
|
|
14356
15004
|
sourceInfo.sourceKind
|
|
@@ -15235,8 +15883,17 @@ async function publishBackendReleaseResources(
|
|
|
15235
15883
|
if (!result.backendRelease.stagedResource) {
|
|
15236
15884
|
fail('Backend Release stage-only 响应缺少 verified stagedResource');
|
|
15237
15885
|
}
|
|
15238
|
-
result.
|
|
15239
|
-
result.
|
|
15886
|
+
result.stagedResources = result.stagedResources || [];
|
|
15887
|
+
result.stagedResources.push(result.backendRelease.stagedResource);
|
|
15888
|
+
if (result.stagedResources.length === 1) {
|
|
15889
|
+
result.stagedResource = result.backendRelease.stagedResource;
|
|
15890
|
+
} else {
|
|
15891
|
+
delete result.stagedResource;
|
|
15892
|
+
}
|
|
15893
|
+
result.handledSelectors = unique([
|
|
15894
|
+
...(result.handledSelectors || []),
|
|
15895
|
+
...handledSelectors,
|
|
15896
|
+
]).sort();
|
|
15240
15897
|
return;
|
|
15241
15898
|
}
|
|
15242
15899
|
const activated = await requestWithAuth(
|
|
@@ -15323,6 +15980,176 @@ async function publishBackendReleaseResources(
|
|
|
15323
15980
|
result.handledSelectors = handledSelectors;
|
|
15324
15981
|
}
|
|
15325
15982
|
|
|
15983
|
+
async function publishWorkflowReleaseResources(
|
|
15984
|
+
config,
|
|
15985
|
+
target,
|
|
15986
|
+
manifest,
|
|
15987
|
+
result,
|
|
15988
|
+
options = {}
|
|
15989
|
+
) {
|
|
15990
|
+
if (!options.stageWorkflowReleaseOnly) return;
|
|
15991
|
+
const candidates = (manifest.workflows || [])
|
|
15992
|
+
.map(item => {
|
|
15993
|
+
const code = item.code || item.resourceCode;
|
|
15994
|
+
const action = getPlannedResourceAction(options, 'workflow', code);
|
|
15995
|
+
return { item, code, action };
|
|
15996
|
+
})
|
|
15997
|
+
.filter(
|
|
15998
|
+
candidate =>
|
|
15999
|
+
candidate.action &&
|
|
16000
|
+
['create', 'update', 'noop'].includes(candidate.action.action) &&
|
|
16001
|
+
!(candidate.action.action === 'noop' && options.skipNoop !== false)
|
|
16002
|
+
);
|
|
16003
|
+
if (candidates.length === 0) {
|
|
16004
|
+
fail('--stage-only 没有匹配到可冻结的 Workflow 资源');
|
|
16005
|
+
}
|
|
16006
|
+
const head = await requestWithAuth(
|
|
16007
|
+
config,
|
|
16008
|
+
target.profileName,
|
|
16009
|
+
workflowReleaseApiPath(target, '/head')
|
|
16010
|
+
);
|
|
16011
|
+
const releaseContext = await ensureOwnedImmutableReleasePublishContext(
|
|
16012
|
+
config,
|
|
16013
|
+
target,
|
|
16014
|
+
options.publishLeaseFlags || {},
|
|
16015
|
+
'Workflow 原子发布',
|
|
16016
|
+
'Workflow Release'
|
|
16017
|
+
);
|
|
16018
|
+
options.publishLease = releaseContext.publishLease;
|
|
16019
|
+
options.publishLeaseId = releaseContext.publishLease.leaseId;
|
|
16020
|
+
const resources = await runWithConcurrency(
|
|
16021
|
+
candidates,
|
|
16022
|
+
Math.min(6, candidates.length),
|
|
16023
|
+
async candidate => {
|
|
16024
|
+
const compiled = await compileWorkflowSourceFile(candidate.item);
|
|
16025
|
+
const definitionJson =
|
|
16026
|
+
compiled?.definitionJson ||
|
|
16027
|
+
(await resolveManifestJson(
|
|
16028
|
+
config,
|
|
16029
|
+
target.profileName,
|
|
16030
|
+
candidate.item,
|
|
16031
|
+
'definitionJson',
|
|
16032
|
+
'definitionFile'
|
|
16033
|
+
));
|
|
16034
|
+
const viewJson =
|
|
16035
|
+
compiled?.previewJson ??
|
|
16036
|
+
(await resolveManifestJson(
|
|
16037
|
+
config,
|
|
16038
|
+
target.profileName,
|
|
16039
|
+
candidate.item,
|
|
16040
|
+
'viewJson',
|
|
16041
|
+
'viewFile',
|
|
16042
|
+
true
|
|
16043
|
+
)) ??
|
|
16044
|
+
(await resolveManifestJson(
|
|
16045
|
+
config,
|
|
16046
|
+
target.profileName,
|
|
16047
|
+
candidate.item,
|
|
16048
|
+
'previewJson',
|
|
16049
|
+
'previewFile',
|
|
16050
|
+
true
|
|
16051
|
+
));
|
|
16052
|
+
return {
|
|
16053
|
+
code: candidate.code,
|
|
16054
|
+
operation:
|
|
16055
|
+
candidate.action.action === 'create' ? 'create' : 'update',
|
|
16056
|
+
formUuid: resolveManifestFormUuid(target.bound, candidate.item),
|
|
16057
|
+
definitionJson,
|
|
16058
|
+
...(viewJson !== undefined ? { viewJson } : {}),
|
|
16059
|
+
publish: candidate.item.publish !== false,
|
|
16060
|
+
};
|
|
16061
|
+
}
|
|
16062
|
+
);
|
|
16063
|
+
const prepared = await requestWithAuth(
|
|
16064
|
+
config,
|
|
16065
|
+
target.profileName,
|
|
16066
|
+
workflowReleaseApiPath(target),
|
|
16067
|
+
{
|
|
16068
|
+
method: 'POST',
|
|
16069
|
+
body: {
|
|
16070
|
+
...releaseContext.control,
|
|
16071
|
+
sourceRepositoryId: releaseContext.sourceRepositoryId,
|
|
16072
|
+
sourceRevision: releaseContext.sourceRevision,
|
|
16073
|
+
parentReleaseId: head?.activeWorkflowReleaseId || null,
|
|
16074
|
+
resources,
|
|
16075
|
+
},
|
|
16076
|
+
}
|
|
16077
|
+
);
|
|
16078
|
+
if (!prepared?.id || !/^[a-f0-9]{64}$/i.test(prepared?.manifestHash || '')) {
|
|
16079
|
+
fail('Workflow Release prepare 响应缺少 id 或 manifestHash');
|
|
16080
|
+
}
|
|
16081
|
+
const preparedCodes = (prepared.resources || [])
|
|
16082
|
+
.map(resource => String(resource.code || ''))
|
|
16083
|
+
.sort();
|
|
16084
|
+
const expectedCodes = candidates.map(candidate => candidate.code).sort();
|
|
16085
|
+
if (!exactStringSet(preparedCodes, expectedCodes)) {
|
|
16086
|
+
fail('Workflow Release prepare 返回的资源集合与精确 selector 不一致');
|
|
16087
|
+
}
|
|
16088
|
+
const verified = await requestWithAuth(
|
|
16089
|
+
config,
|
|
16090
|
+
target.profileName,
|
|
16091
|
+
workflowReleaseApiPath(
|
|
16092
|
+
target,
|
|
16093
|
+
`/${encodeURIComponent(prepared.id)}/verify`
|
|
16094
|
+
),
|
|
16095
|
+
{
|
|
16096
|
+
method: 'POST',
|
|
16097
|
+
body: {
|
|
16098
|
+
...releaseContext.control,
|
|
16099
|
+
manifestHash: prepared.manifestHash,
|
|
16100
|
+
},
|
|
16101
|
+
}
|
|
16102
|
+
);
|
|
16103
|
+
if (String(verified?.status || '').toLowerCase() !== 'verified') {
|
|
16104
|
+
fail('Workflow Release stage-only 未获得 verified 状态');
|
|
16105
|
+
}
|
|
16106
|
+
const handledSelectors = candidates
|
|
16107
|
+
.map(candidate => `workflow:${candidate.code}`)
|
|
16108
|
+
.sort();
|
|
16109
|
+
const stagedResource = {
|
|
16110
|
+
kind: 'WorkflowRelease',
|
|
16111
|
+
identity: { releaseId: verified.id || prepared.id },
|
|
16112
|
+
action: 'update',
|
|
16113
|
+
hash: verified.manifestHash || prepared.manifestHash,
|
|
16114
|
+
revision: {
|
|
16115
|
+
releaseId: verified.id || prepared.id,
|
|
16116
|
+
manifestHash: verified.manifestHash || prepared.manifestHash,
|
|
16117
|
+
sourceRevision: verified.sourceRevision || prepared.sourceRevision,
|
|
16118
|
+
},
|
|
16119
|
+
metadata: {
|
|
16120
|
+
contractVersion: 'workflow_release_v1',
|
|
16121
|
+
handledSelectors,
|
|
16122
|
+
resourceCount: candidates.length,
|
|
16123
|
+
},
|
|
16124
|
+
};
|
|
16125
|
+
result.stagedResources = result.stagedResources || [];
|
|
16126
|
+
result.stagedResources.push(stagedResource);
|
|
16127
|
+
result.workflowRelease = {
|
|
16128
|
+
id: verified.id || prepared.id,
|
|
16129
|
+
manifestHash: verified.manifestHash || prepared.manifestHash,
|
|
16130
|
+
status: verified.status,
|
|
16131
|
+
handledSelectors,
|
|
16132
|
+
stagedResource,
|
|
16133
|
+
};
|
|
16134
|
+
result.handledSelectors = unique([
|
|
16135
|
+
...(result.handledSelectors || []),
|
|
16136
|
+
...handledSelectors,
|
|
16137
|
+
]).sort();
|
|
16138
|
+
options.workflowReleaseHandledKeys = new Set(
|
|
16139
|
+
candidates.map(candidate => `Workflow:${candidate.code}`)
|
|
16140
|
+
);
|
|
16141
|
+
for (const candidate of candidates) {
|
|
16142
|
+
result.published.push({
|
|
16143
|
+
kind: 'workflow',
|
|
16144
|
+
code: candidate.code,
|
|
16145
|
+
action: 'stage',
|
|
16146
|
+
writeMode: 'workflow-release-stage',
|
|
16147
|
+
workflowReleaseId: stagedResource.identity.releaseId,
|
|
16148
|
+
activation: 'staged',
|
|
16149
|
+
});
|
|
16150
|
+
}
|
|
16151
|
+
}
|
|
16152
|
+
|
|
15326
16153
|
async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
15327
16154
|
const result = {
|
|
15328
16155
|
appType: target.appType,
|
|
@@ -15347,6 +16174,13 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
15347
16174
|
await publishMenuResources(config, target, manifest.menus || [], result, publishOptions);
|
|
15348
16175
|
await publishConnectorResources(config, target, manifest.connectors || [], result, publishOptions);
|
|
15349
16176
|
await publishNotificationResources(config, target, manifest.notifications || [], result, publishOptions);
|
|
16177
|
+
await publishWorkflowReleaseResources(
|
|
16178
|
+
config,
|
|
16179
|
+
target,
|
|
16180
|
+
manifest,
|
|
16181
|
+
result,
|
|
16182
|
+
publishOptions
|
|
16183
|
+
);
|
|
15350
16184
|
await publishWorkflowResources(config, target, manifest.workflows || [], result, publishOptions);
|
|
15351
16185
|
await publishBackendReleaseResources(
|
|
15352
16186
|
config,
|
|
@@ -15430,7 +16264,9 @@ function inferManifestSourceKind(item, kind) {
|
|
|
15430
16264
|
if (candidates.some(value => value.includes('/js-code-nodes/'))) return 'js-code-nodes';
|
|
15431
16265
|
if (candidates.some(value => value.includes('/automations/'))) return 'automations';
|
|
15432
16266
|
if (candidates.some(value => value.includes('/functions/'))) return 'functions';
|
|
15433
|
-
|
|
16267
|
+
if (kind === 'Function') return 'functions';
|
|
16268
|
+
if (kind === 'Workflow') return 'workflows';
|
|
16269
|
+
return 'automations';
|
|
15434
16270
|
}
|
|
15435
16271
|
|
|
15436
16272
|
function buildResourceChangeBaselineTargets(manifest, actions = [], options = {}) {
|
|
@@ -15441,7 +16277,12 @@ function buildResourceChangeBaselineTargets(manifest, actions = [], options = {}
|
|
|
15441
16277
|
for (const item of items || []) {
|
|
15442
16278
|
const code = item.code || item.functionCode || item.resourceCode;
|
|
15443
16279
|
if (!code) continue;
|
|
15444
|
-
const planKind =
|
|
16280
|
+
const planKind =
|
|
16281
|
+
kind === 'Function'
|
|
16282
|
+
? 'function'
|
|
16283
|
+
: kind === 'Workflow'
|
|
16284
|
+
? 'workflow'
|
|
16285
|
+
: 'automation';
|
|
15445
16286
|
const action = actionIndex.get(`${planKind}:${code}`);
|
|
15446
16287
|
if (action?.action === 'create') continue;
|
|
15447
16288
|
if (action?.action === 'noop' && !options.force) continue;
|
|
@@ -15457,6 +16298,7 @@ function buildResourceChangeBaselineTargets(manifest, actions = [], options = {}
|
|
|
15457
16298
|
};
|
|
15458
16299
|
append('Function', manifest.functions);
|
|
15459
16300
|
append('Automation', manifest.automations);
|
|
16301
|
+
append('Workflow', manifest.workflows);
|
|
15460
16302
|
for (const action of actions || []) {
|
|
15461
16303
|
if (action?.action !== 'delete' || !action.code) continue;
|
|
15462
16304
|
const kind =
|
|
@@ -15464,6 +16306,8 @@ function buildResourceChangeBaselineTargets(manifest, actions = [], options = {}
|
|
|
15464
16306
|
? 'Function'
|
|
15465
16307
|
: action.kind === 'automation'
|
|
15466
16308
|
? 'Automation'
|
|
16309
|
+
: action.kind === 'workflow'
|
|
16310
|
+
? 'Workflow'
|
|
15467
16311
|
: null;
|
|
15468
16312
|
if (!kind) continue;
|
|
15469
16313
|
const key = resourceBaselineTargetKey(kind, action.code);
|
|
@@ -15472,7 +16316,12 @@ function buildResourceChangeBaselineTargets(manifest, actions = [], options = {}
|
|
|
15472
16316
|
targets.push({
|
|
15473
16317
|
kind,
|
|
15474
16318
|
code: action.code,
|
|
15475
|
-
sourceKind:
|
|
16319
|
+
sourceKind:
|
|
16320
|
+
kind === 'Function'
|
|
16321
|
+
? 'functions'
|
|
16322
|
+
: kind === 'Workflow'
|
|
16323
|
+
? 'workflows'
|
|
16324
|
+
: 'automations',
|
|
15476
16325
|
});
|
|
15477
16326
|
}
|
|
15478
16327
|
return targets;
|
|
@@ -15484,7 +16333,6 @@ const GENERIC_RESOURCE_KEY_BY_PLAN_KIND = Object.freeze({
|
|
|
15484
16333
|
connector: 'connectors',
|
|
15485
16334
|
notificationTemplate: 'notifications',
|
|
15486
16335
|
notificationTypeConfig: 'notifications',
|
|
15487
|
-
workflow: 'workflows',
|
|
15488
16336
|
authConfig: 'authConfigs',
|
|
15489
16337
|
route: 'routes',
|
|
15490
16338
|
publicAccessPolicy: 'publicAccessPolicies',
|
|
@@ -15582,13 +16430,22 @@ function runGenericResourceGitBasePreflight({
|
|
|
15582
16430
|
publishLeaseId,
|
|
15583
16431
|
headDigest,
|
|
15584
16432
|
baseCommit,
|
|
16433
|
+
appType: target.appType,
|
|
16434
|
+
profile: target.profileName,
|
|
16435
|
+
repository: baseline?.sourceBase?.repo,
|
|
16436
|
+
resourceHeads: baseline?.resourceHeads,
|
|
15585
16437
|
selectors,
|
|
15586
16438
|
typeFilters,
|
|
15587
16439
|
});
|
|
15588
|
-
let basePlan =
|
|
15589
|
-
|
|
16440
|
+
let basePlan =
|
|
16441
|
+
genericGitBasePlanCache.get(cacheKey) ||
|
|
16442
|
+
readContentCache({
|
|
16443
|
+
cwd: process.cwd(),
|
|
16444
|
+
namespace: 'generic-git-base-plan',
|
|
16445
|
+
key: cacheKey,
|
|
16446
|
+
});
|
|
16447
|
+
let cached = Boolean(basePlan);
|
|
15590
16448
|
if (!basePlan) {
|
|
15591
|
-
cached = false;
|
|
15592
16449
|
basePlan = withGitBaseWorkspace(
|
|
15593
16450
|
{
|
|
15594
16451
|
cwd: process.cwd(),
|
|
@@ -15659,6 +16516,20 @@ function runGenericResourceGitBasePreflight({
|
|
|
15659
16516
|
}
|
|
15660
16517
|
);
|
|
15661
16518
|
genericGitBasePlanCache.set(cacheKey, basePlan);
|
|
16519
|
+
writeContentCache({
|
|
16520
|
+
cwd: process.cwd(),
|
|
16521
|
+
namespace: 'generic-git-base-plan',
|
|
16522
|
+
key: cacheKey,
|
|
16523
|
+
value: {
|
|
16524
|
+
actions: (basePlan.actions || [])
|
|
16525
|
+
.filter(action => action?.kind && action?.code && action?.action)
|
|
16526
|
+
.map(action => ({
|
|
16527
|
+
kind: action.kind,
|
|
16528
|
+
code: action.code,
|
|
16529
|
+
action: action.action,
|
|
16530
|
+
})),
|
|
16531
|
+
},
|
|
16532
|
+
});
|
|
15662
16533
|
while (genericGitBasePlanCache.size > 32) {
|
|
15663
16534
|
genericGitBasePlanCache.delete(genericGitBasePlanCache.keys().next().value);
|
|
15664
16535
|
}
|
|
@@ -15790,7 +16661,7 @@ async function runResourceChangeBaselinePreflight(
|
|
|
15790
16661
|
ok: true,
|
|
15791
16662
|
baselineId: baseline.baselineId,
|
|
15792
16663
|
checkedTargets: [],
|
|
15793
|
-
skipped: 'no-existing-function-or-
|
|
16664
|
+
skipped: 'no-existing-function-automation-or-workflow-writes',
|
|
15794
16665
|
};
|
|
15795
16666
|
} else {
|
|
15796
16667
|
const artifactCache = { ...(baseline.localBaseArtifactHashes || {}) };
|
|
@@ -16995,6 +17866,13 @@ async function publishNotificationResources(config, target, notifications, resul
|
|
|
16995
17866
|
|
|
16996
17867
|
async function publishWorkflowResources(config, target, workflows, result, options = {}) {
|
|
16997
17868
|
for (const workflowItem of workflows) {
|
|
17869
|
+
if (
|
|
17870
|
+
options.workflowReleaseHandledKeys?.has(
|
|
17871
|
+
`Workflow:${workflowItem.code}`
|
|
17872
|
+
)
|
|
17873
|
+
) {
|
|
17874
|
+
continue;
|
|
17875
|
+
}
|
|
16998
17876
|
if (shouldSkipNoopResource(options, 'workflow', workflowItem.code)) {
|
|
16999
17877
|
recordNoopResource(result, 'workflow', workflowItem.code, options);
|
|
17000
17878
|
continue;
|
|
@@ -21237,6 +22115,27 @@ function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind)
|
|
|
21237
22115
|
);
|
|
21238
22116
|
}
|
|
21239
22117
|
|
|
22118
|
+
function runLegacyWorkspaceJsCodeBuild(
|
|
22119
|
+
workspaceRoot,
|
|
22120
|
+
resolvedScriptCode,
|
|
22121
|
+
sourceKind
|
|
22122
|
+
) {
|
|
22123
|
+
return spawnSync(
|
|
22124
|
+
'pnpm',
|
|
22125
|
+
[
|
|
22126
|
+
'build-js-code',
|
|
22127
|
+
'--script',
|
|
22128
|
+
resolvedScriptCode,
|
|
22129
|
+
'--source',
|
|
22130
|
+
sourceKind,
|
|
22131
|
+
],
|
|
22132
|
+
{
|
|
22133
|
+
cwd: workspaceRoot,
|
|
22134
|
+
encoding: 'utf8',
|
|
22135
|
+
}
|
|
22136
|
+
);
|
|
22137
|
+
}
|
|
22138
|
+
|
|
21240
22139
|
function isUnsupportedFunctionsBuildScript(result) {
|
|
21241
22140
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
21242
22141
|
return (
|
|
@@ -21668,6 +22567,79 @@ function isRuntimeSourceFile(file) {
|
|
|
21668
22567
|
return /\.(ts|tsx|js|jsx|mjs|cjs|css|scss|sass|less|html|json|svg|png|jpe?g|gif|webp|avif|ico)$/i.test(file);
|
|
21669
22568
|
}
|
|
21670
22569
|
|
|
22570
|
+
function resourceSpecForWorkspaceFile(file) {
|
|
22571
|
+
const prefix = 'src/resources/';
|
|
22572
|
+
if (!file.startsWith(prefix)) return null;
|
|
22573
|
+
const relative = file.slice(prefix.length);
|
|
22574
|
+
return (
|
|
22575
|
+
RESOURCE_SPECS.find(spec => {
|
|
22576
|
+
const dir = String(spec.dir || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
22577
|
+
const topFiles = (spec.topFiles || []).map(item =>
|
|
22578
|
+
String(item).replace(/\\/g, '/')
|
|
22579
|
+
);
|
|
22580
|
+
return (
|
|
22581
|
+
topFiles.includes(relative) ||
|
|
22582
|
+
(dir && (relative === dir || relative.startsWith(`${dir}/`)))
|
|
22583
|
+
);
|
|
22584
|
+
}) || null
|
|
22585
|
+
);
|
|
22586
|
+
}
|
|
22587
|
+
|
|
22588
|
+
function changedResourceCodesForFile(file, spec, baseRef = 'HEAD') {
|
|
22589
|
+
const absolute = path.join(process.cwd(), file);
|
|
22590
|
+
const currentItems = fs.existsSync(absolute)
|
|
22591
|
+
? readResourceItemsFromFile(absolute, spec)
|
|
22592
|
+
: [];
|
|
22593
|
+
const baseItems = readResourceItemsFromGit(baseRef, file, spec) || [];
|
|
22594
|
+
const current = indexResourceItemsForDiff(currentItems);
|
|
22595
|
+
const base = indexResourceItemsForDiff(baseItems);
|
|
22596
|
+
const codes = unique([...current.keys(), ...base.keys()]).filter(
|
|
22597
|
+
code => current.get(code) !== base.get(code)
|
|
22598
|
+
);
|
|
22599
|
+
if (codes.length > 0) return codes.sort();
|
|
22600
|
+
if (currentItems.some(item => item?.__invalid) || baseItems.some(item => item?.__invalid)) {
|
|
22601
|
+
return ['*'];
|
|
22602
|
+
}
|
|
22603
|
+
return [];
|
|
22604
|
+
}
|
|
22605
|
+
|
|
22606
|
+
function classifyGenericResourceFiles(files, baseRef = 'HEAD') {
|
|
22607
|
+
const resourceSelectors = {};
|
|
22608
|
+
const formSettings = new Set();
|
|
22609
|
+
const unknown = new Set();
|
|
22610
|
+
const codeResourceKeys = new Set([
|
|
22611
|
+
'functions',
|
|
22612
|
+
'automations',
|
|
22613
|
+
'workflows',
|
|
22614
|
+
]);
|
|
22615
|
+
for (const file of files || []) {
|
|
22616
|
+
if (!file.startsWith('src/resources/')) continue;
|
|
22617
|
+
const spec = resourceSpecForWorkspaceFile(file);
|
|
22618
|
+
if (!spec) {
|
|
22619
|
+
unknown.add(file);
|
|
22620
|
+
continue;
|
|
22621
|
+
}
|
|
22622
|
+
if (codeResourceKeys.has(spec.key)) continue;
|
|
22623
|
+
const codes = changedResourceCodesForFile(file, spec, baseRef);
|
|
22624
|
+
if (spec.key === 'formSettings') {
|
|
22625
|
+
for (const code of codes) formSettings.add(code);
|
|
22626
|
+
continue;
|
|
22627
|
+
}
|
|
22628
|
+
if (codes.length === 0) continue;
|
|
22629
|
+
resourceSelectors[spec.key] = unique([
|
|
22630
|
+
...(resourceSelectors[spec.key] || []),
|
|
22631
|
+
...codes,
|
|
22632
|
+
]).sort();
|
|
22633
|
+
}
|
|
22634
|
+
if (unknown.size > 0) {
|
|
22635
|
+
resourceSelectors.unknown = Array.from(unknown).sort();
|
|
22636
|
+
}
|
|
22637
|
+
return {
|
|
22638
|
+
formSettings: Array.from(formSettings).sort(),
|
|
22639
|
+
resourceSelectors,
|
|
22640
|
+
};
|
|
22641
|
+
}
|
|
22642
|
+
|
|
21671
22643
|
function classifyWorkspaceFiles(files, runtimeMode, options = {}) {
|
|
21672
22644
|
const forms = new Set();
|
|
21673
22645
|
const pages = new Set();
|
|
@@ -21685,6 +22657,11 @@ function classifyWorkspaceFiles(files, runtimeMode, options = {}) {
|
|
|
21685
22657
|
jsCodeNodes,
|
|
21686
22658
|
};
|
|
21687
22659
|
const dependencyTargets = getWorkspaceImpactedResourceTargets(files);
|
|
22660
|
+
const genericTargets = classifyGenericResourceFiles(
|
|
22661
|
+
files,
|
|
22662
|
+
options.baseRef || 'HEAD'
|
|
22663
|
+
);
|
|
22664
|
+
for (const code of genericTargets.formSettings) forms.add(code);
|
|
21688
22665
|
for (const [key, codes] of Object.entries(dependencyTargets)) {
|
|
21689
22666
|
for (const code of codes) codeResourceTargets[key]?.add(code);
|
|
21690
22667
|
}
|
|
@@ -21729,7 +22706,7 @@ function classifyWorkspaceFiles(files, runtimeMode, options = {}) {
|
|
|
21729
22706
|
continue;
|
|
21730
22707
|
}
|
|
21731
22708
|
if (file.startsWith('src/resources/')) {
|
|
21732
|
-
resources = true;
|
|
22709
|
+
if (!resourceSpecForWorkspaceFile(file)) resources = true;
|
|
21733
22710
|
continue;
|
|
21734
22711
|
}
|
|
21735
22712
|
if (
|
|
@@ -21753,6 +22730,7 @@ function classifyWorkspaceFiles(files, runtimeMode, options = {}) {
|
|
|
21753
22730
|
workflows: Array.from(workflows).sort(),
|
|
21754
22731
|
jsCodeNodes: Array.from(jsCodeNodes).sort(),
|
|
21755
22732
|
resources,
|
|
22733
|
+
resourceSelectors: genericTargets.resourceSelectors,
|
|
21756
22734
|
runtime,
|
|
21757
22735
|
other,
|
|
21758
22736
|
};
|
|
@@ -21926,17 +22904,45 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21926
22904
|
? getWorkspaceChangedFiles({ since: options.since })
|
|
21927
22905
|
: getAllWorkspaceSourceFiles();
|
|
21928
22906
|
const targets = changeScope
|
|
21929
|
-
? {
|
|
21930
|
-
|
|
22907
|
+
? (() => {
|
|
22908
|
+
const inferred = classifyWorkspaceFiles(files, runtimeMode, {
|
|
22909
|
+
baseRef:
|
|
22910
|
+
options.since ||
|
|
22911
|
+
changeScope.change?.sourceBase?.baseCommit ||
|
|
22912
|
+
'HEAD',
|
|
22913
|
+
});
|
|
22914
|
+
return {
|
|
22915
|
+
forms: unique([
|
|
22916
|
+
...(changeScope.targets.forms || []),
|
|
22917
|
+
...(inferred.forms || []),
|
|
22918
|
+
]).sort(),
|
|
21931
22919
|
pages: [...(changeScope.targets.pages || [])],
|
|
21932
22920
|
functions: [...(changeScope.targets.functions || [])],
|
|
21933
22921
|
automations: [...(changeScope.targets.automations || [])],
|
|
21934
22922
|
workflows: [...(changeScope.targets.workflows || [])],
|
|
21935
22923
|
jsCodeNodes: [...(changeScope.targets.jsCodeNodes || [])],
|
|
21936
|
-
resources:
|
|
22924
|
+
resources:
|
|
22925
|
+
Boolean(changeScope.targets.resources) &&
|
|
22926
|
+
(
|
|
22927
|
+
Object.keys(inferred.resourceSelectors || {}).length === 0 ||
|
|
22928
|
+
Boolean(inferred.resourceSelectors?.unknown?.length)
|
|
22929
|
+
),
|
|
22930
|
+
resourceSelectors: Object.fromEntries(
|
|
22931
|
+
unique([
|
|
22932
|
+
...Object.keys(changeScope.targets.resourceSelectors || {}),
|
|
22933
|
+
...Object.keys(inferred.resourceSelectors || {}),
|
|
22934
|
+
]).map(key => [
|
|
22935
|
+
key,
|
|
22936
|
+
unique([
|
|
22937
|
+
...(changeScope.targets.resourceSelectors?.[key] || []),
|
|
22938
|
+
...(inferred.resourceSelectors?.[key] || []),
|
|
22939
|
+
]).sort(),
|
|
22940
|
+
])
|
|
22941
|
+
),
|
|
21937
22942
|
runtime: Boolean(changeScope.targets.runtime),
|
|
21938
22943
|
other: [...(changeScope.targets.other || [])],
|
|
21939
|
-
|
|
22944
|
+
};
|
|
22945
|
+
})()
|
|
21940
22946
|
: classifyWorkspaceFiles(files, runtimeMode, {
|
|
21941
22947
|
baseRef: options.since || 'HEAD',
|
|
21942
22948
|
});
|
|
@@ -21996,12 +23002,22 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21996
23002
|
targets,
|
|
21997
23003
|
runtimeMode
|
|
21998
23004
|
);
|
|
21999
|
-
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
|
|
22003
|
-
|
|
22004
|
-
|
|
23005
|
+
let generatedCommands = [];
|
|
23006
|
+
try {
|
|
23007
|
+
generatedCommands = buildWorkspaceReleaseCommands(
|
|
23008
|
+
targets,
|
|
23009
|
+
runtimeMode,
|
|
23010
|
+
options.profile,
|
|
23011
|
+
changeScope?.changeId
|
|
23012
|
+
);
|
|
23013
|
+
} catch (error) {
|
|
23014
|
+
if (error?.code !== 'RELEASE_ATOMIC_TARGET_UNSUPPORTED') throw error;
|
|
23015
|
+
errors.push({
|
|
23016
|
+
name: 'release-atomic-target-unsupported',
|
|
23017
|
+
message: error.message,
|
|
23018
|
+
unsupportedTargets: error.unsupportedTargets || [],
|
|
23019
|
+
});
|
|
23020
|
+
}
|
|
22005
23021
|
const knownTargetCount =
|
|
22006
23022
|
targets.forms.length +
|
|
22007
23023
|
targets.pages.length +
|
|
@@ -22111,6 +23127,10 @@ function printWorkspaceReleasePlan(plan) {
|
|
|
22111
23127
|
`workflows: ${(plan.targets.workflows || []).length ? plan.targets.workflows.join(', ') : '(none)'}`,
|
|
22112
23128
|
`jsCodeNodes: ${(plan.targets.jsCodeNodes || []).length ? plan.targets.jsCodeNodes.join(', ') : '(none)'}`,
|
|
22113
23129
|
`resources: ${plan.targets.resources ? 'yes' : 'no'}`,
|
|
23130
|
+
...Object.entries(plan.targets.resourceSelectors || {}).map(
|
|
23131
|
+
([type, codes]) =>
|
|
23132
|
+
`resource ${type}: ${codes.length ? codes.join(', ') : '(none)'}`
|
|
23133
|
+
),
|
|
22114
23134
|
`runtime deploy: ${plan.targets.runtime ? 'yes' : 'no'}`,
|
|
22115
23135
|
];
|
|
22116
23136
|
if (plan.commands.length > 0) {
|