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/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
- ' - 新工作区使用 openxiangda-sdd-v2:proposal/specs/design/tasks/evidence 给人读,coverage.json/release.json 给 CLI 校验。',
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: Boolean(releasePlan),
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({ cwd: process.cwd(), changeId });
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 runOpenXiangdaChild(args, options = {}) {
1027
- return new Promise((resolve, reject) => {
1028
- const bin = path.join(__dirname, '..', 'bin', 'openxiangda.js');
1029
- const child = spawn(process.execPath, [bin, ...args, '--json'], {
1030
- cwd: process.cwd(),
1031
- env: process.env,
1032
- stdio: ['ignore', 'pipe', 'pipe'],
1033
- });
1034
- let stdout = '';
1035
- let stderr = '';
1036
- child.stdout.on('data', chunk => {
1037
- stdout += String(chunk);
1038
- });
1039
- child.stderr.on('data', chunk => {
1040
- const text = String(chunk);
1041
- stderr += text;
1042
- if (!options.quiet) warn(maskText(text).trimEnd());
1043
- });
1044
- child.on('error', reject);
1045
- child.on('close', code => {
1046
- if (code === 0) {
1047
- resolve(parseChildJsonOutput(stdout));
1048
- return;
1049
- }
1050
- const message = maskText(
1051
- String(stderr || stdout || `openxiangda ${args.join(' ')} failed`)
1052
- ).trim();
1053
- const error = new Error(message);
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 = code;
1058
- reject(error);
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: true,
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 runOpenXiangdaChild(step.args);
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 runOpenXiangdaChild(
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 sourceRevision = baseline?.releaseSourceRevision;
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
- return hasBackendReleaseV2
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-detail') {
3381
+ } else if (subcommand === 'app-retry') {
2889
3382
  if (!releaseId) {
2890
- fail('用法: openxiangda release app-detail <releaseId> [--profile name]');
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(target, `/${encodeURIComponent(releaseId)}`)
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
- saveRuntimeHeadState(target, appHead);
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 <code> --stage-only --profile <name>',
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 Backend Release;随后把返回的 stagedResource 交给 release app-finalize 原子激活。',
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: Boolean(flags['stage-only']),
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(type => !['functions', 'automations'].includes(type))
10388
+ normalized.some(
10389
+ type => !['functions', 'automations', 'workflows'].includes(type)
10390
+ )
9799
10391
  ) {
9800
- fail('--stage-only 必须显式选择 function 或 automation 资源类型');
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
- if (flags.json) return writeJson(data);
9988
- print(JSON.stringify(data, null, 2));
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
- printRuntimeProgress(
10572
- `runtime file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
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
- printRuntimeProgress(
10629
- `runtime ${formatRuntimeStorageProvider(storageProvider)} file uploaded [${completed}/${files.length}] ${file.path} ${formatBytes(file.size)} traceId=${options.traceId}`
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 || app?.activeRuntime?.releaseId || null;
11492
+ app?.activeRuntimeReleaseId ||
11493
+ app?.activeRuntime?.releaseId ||
11494
+ fallbackRelease?.id ||
11495
+ fallbackRelease?.releaseId ||
11496
+ null;
10895
11497
  const activeBuildId =
10896
- app?.activeRuntimeBuildId || app?.activeRuntime?.buildId || null;
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 || app?.activeRuntime?.assetBaseUrl || null,
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 = runWorkspaceJsCodeBuild(
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.stagedResource = result.backendRelease.stagedResource;
15239
- result.handledSelectors = handledSelectors;
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
- return kind === 'Function' ? 'functions' : 'automations';
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 = kind === 'Function' ? 'function' : 'automation';
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: kind === 'Function' ? 'functions' : 'automations',
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 = genericGitBasePlanCache.get(cacheKey);
15589
- let cached = true;
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-automation-writes',
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
- forms: [...(changeScope.targets.forms || [])],
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: Boolean(changeScope.targets.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
- const generatedCommands = buildWorkspaceReleaseCommands(
22000
- targets,
22001
- runtimeMode,
22002
- options.profile,
22003
- changeScope?.changeId
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) {