openxiangda 1.0.165 → 1.0.166

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
@@ -990,6 +990,7 @@ async function release(args) {
990
990
  );
991
991
  }
992
992
  const pendingBaseline = getStoredChangeBaseline(target);
993
+ const newReleaseLifecycle = !stored && !pendingBaseline;
993
994
  const clientSessionId =
994
995
  existing?.clientSessionId ||
995
996
  (pendingBaseline?.changeId === changeId &&
@@ -1018,12 +1019,16 @@ async function release(args) {
1018
1019
  throw error;
1019
1020
  }
1020
1021
  const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
1022
+ const stagedResourcesFile = newReleaseLifecycle
1023
+ ? initializeStagedAppReleaseResources(target, changeId)
1024
+ : null;
1021
1025
  const result = {
1022
1026
  ...lease,
1023
1027
  baselineId: finalizedBaseline?.baselineId,
1024
1028
  baselineHeadDigest: finalizedBaseline?.headDigest,
1025
1029
  sourceBase: finalizedBaseline?.sourceBase,
1026
1030
  releaseSourceRevision: finalizedBaseline?.releaseSourceRevision,
1031
+ ...(stagedResourcesFile ? { stagedResourcesFile } : {}),
1027
1032
  };
1028
1033
  if (flags.json) return writeJson(result);
1029
1034
  print(
@@ -1562,6 +1567,270 @@ function readStagedAppReleaseResources(flags = {}) {
1562
1567
  );
1563
1568
  }
1564
1569
 
1570
+ function stagedResourcesFileForChange(changeId) {
1571
+ const normalized = String(changeId || '').trim();
1572
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) {
1573
+ fail('APP_RELEASE_STAGED_RESOURCES_CHANGE_INVALID: 自动聚合 staged resources 需要有效 --change');
1574
+ }
1575
+ return path.join(
1576
+ process.cwd(),
1577
+ '.openxiangda',
1578
+ 'releases',
1579
+ normalized,
1580
+ 'staged-resources.json'
1581
+ );
1582
+ }
1583
+
1584
+ function stagedResourcesContextFileForChange(changeId) {
1585
+ return stagedResourcesFileForChange(changeId).replace(
1586
+ /staged-resources\.json$/,
1587
+ 'staged-resources.context.json'
1588
+ );
1589
+ }
1590
+
1591
+ function currentStagedResourcesContext(target, changeId) {
1592
+ const baseline = getStoredChangeBaseline(target);
1593
+ if (!baseline?.baselineId || baseline.changeId !== changeId) {
1594
+ fail(
1595
+ `APP_RELEASE_STAGED_CONTEXT_REQUIRED: change ${changeId} 缺少当前发布基线`
1596
+ );
1597
+ }
1598
+ return {
1599
+ contractVersion: 'staged_resources_context_v1',
1600
+ appType: target.appType,
1601
+ profile: target.profileName,
1602
+ changeId,
1603
+ baselineId: baseline.baselineId,
1604
+ clientSessionId: baseline.clientSessionId || null,
1605
+ };
1606
+ }
1607
+
1608
+ function writeStagedResourcesFiles(changeId, resources, context) {
1609
+ const file = stagedResourcesFileForChange(changeId);
1610
+ const contextFile = stagedResourcesContextFileForChange(changeId);
1611
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1612
+ for (const [targetFile, value] of [
1613
+ [file, resources],
1614
+ [contextFile, context],
1615
+ ]) {
1616
+ const temporary = `${targetFile}.${process.pid}.tmp`;
1617
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
1618
+ mode: 0o600,
1619
+ });
1620
+ fs.renameSync(temporary, targetFile);
1621
+ }
1622
+ return path.relative(process.cwd(), file).replace(/\\/g, '/');
1623
+ }
1624
+
1625
+ function initializeStagedAppReleaseResources(target, changeId) {
1626
+ const context = currentStagedResourcesContext(target, changeId);
1627
+ return writeStagedResourcesFiles(changeId, [], {
1628
+ ...context,
1629
+ initializedAt: new Date().toISOString(),
1630
+ });
1631
+ }
1632
+
1633
+ function assertStagedResourcesContext(target, changeId) {
1634
+ const expected = currentStagedResourcesContext(target, changeId);
1635
+ const contextFile = stagedResourcesContextFileForChange(changeId);
1636
+ if (!fs.existsSync(contextFile)) {
1637
+ if (!fs.existsSync(stagedResourcesFileForChange(changeId))) {
1638
+ initializeStagedAppReleaseResources(target, changeId);
1639
+ }
1640
+ }
1641
+ if (!fs.existsSync(contextFile)) {
1642
+ fail(
1643
+ 'APP_RELEASE_STAGED_CONTEXT_REQUIRED: staged resources 缺少当前 release begin 生成的 context;请重新暂存本次 change 的 child releases'
1644
+ );
1645
+ }
1646
+ const actual = JSON.parse(fs.readFileSync(contextFile, 'utf8'));
1647
+ for (const field of [
1648
+ 'contractVersion',
1649
+ 'appType',
1650
+ 'profile',
1651
+ 'changeId',
1652
+ 'baselineId',
1653
+ 'clientSessionId',
1654
+ ]) {
1655
+ if ((actual?.[field] ?? null) !== (expected[field] ?? null)) {
1656
+ fail(
1657
+ `APP_RELEASE_STAGED_CONTEXT_MISMATCH: ${field} 不属于当前 change/baseline/session,请重新暂存 child releases`
1658
+ );
1659
+ }
1660
+ }
1661
+ return actual;
1662
+ }
1663
+
1664
+ function recordStagedAppReleaseResources(
1665
+ changeId,
1666
+ resources,
1667
+ target
1668
+ ) {
1669
+ if (!changeId || !Array.isArray(resources) || resources.length === 0) {
1670
+ return null;
1671
+ }
1672
+ const file = stagedResourcesFileForChange(changeId);
1673
+ const context = assertStagedResourcesContext(target, changeId);
1674
+ let existing = [];
1675
+ if (fs.existsSync(file)) {
1676
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
1677
+ existing = Array.isArray(parsed) && parsed.length === 0
1678
+ ? []
1679
+ : normalizeStagedAppReleaseResources(parsed);
1680
+ }
1681
+ const incoming = normalizeStagedAppReleaseResources(resources);
1682
+ const merged = new Map();
1683
+ for (const resource of [...existing, ...incoming]) {
1684
+ const described = describeAppReleaseResource(resource, 0, 'stagedResources');
1685
+ merged.set(described.stableKey, resource);
1686
+ }
1687
+ const output = Array.from(merged.values()).sort((left, right) => {
1688
+ const leftKey = describeAppReleaseResource(left, 0, 'stagedResources').stableKey;
1689
+ const rightKey = describeAppReleaseResource(right, 0, 'stagedResources').stableKey;
1690
+ return leftKey.localeCompare(rightKey);
1691
+ });
1692
+ return writeStagedResourcesFiles(changeId, output, context);
1693
+ }
1694
+
1695
+ function exactStringSet(left, right) {
1696
+ const normalize = values =>
1697
+ Array.from(
1698
+ new Set((values || []).map(value => String(value || '').trim()).filter(Boolean))
1699
+ ).sort();
1700
+ const a = normalize(left);
1701
+ const b = normalize(right);
1702
+ return a.length === b.length && a.every((value, index) => value === b[index]);
1703
+ }
1704
+
1705
+ function stagedFormCode(target, resource) {
1706
+ const explicit = String(resource?.metadata?.formCode || '').trim();
1707
+ if (explicit) return explicit;
1708
+ const formUuid = String(resource?.identity?.formUuid || '').trim();
1709
+ for (const bucketName of ['formSettings', 'forms']) {
1710
+ for (const [code, entry] of Object.entries(
1711
+ target.bound.resources?.[bucketName] || {}
1712
+ )) {
1713
+ if (String(entry?.formUuid || '').trim() === formUuid) return code;
1714
+ }
1715
+ }
1716
+ return formUuid;
1717
+ }
1718
+
1719
+ function assertStagedAppReleaseScope(target, flags, resources) {
1720
+ const changeId = readStringFlag(flags, 'change');
1721
+ if (!changeId) {
1722
+ fail(
1723
+ 'APP_RELEASE_STAGED_SCOPE_REQUIRED: staged child activation 必须显式提供 --change'
1724
+ );
1725
+ }
1726
+ assertStagedResourcesContext(target, changeId);
1727
+ let scope;
1728
+ try {
1729
+ scope = getSddChangeScope({
1730
+ cwd: process.cwd(),
1731
+ configText: readWorkspaceConfigText(),
1732
+ changeId,
1733
+ });
1734
+ } catch (error) {
1735
+ if (/SDD 未初始化|change 不存在|ENOENT/i.test(String(error?.message || ''))) {
1736
+ return {
1737
+ changeId,
1738
+ scope: null,
1739
+ resources: normalizeStagedAppReleaseResources(resources),
1740
+ compatibility: 'unmanaged_sdd_scope',
1741
+ };
1742
+ }
1743
+ throw error;
1744
+ }
1745
+ const targets = scope.targets || {};
1746
+ const normalized = normalizeStagedAppReleaseResources(resources);
1747
+ const byKind = kind => normalized.filter(resource => resource.kind === kind);
1748
+ const expectedBackendSelectors = [
1749
+ ...(targets.functions || []).map(code => `function:${code}`),
1750
+ ...(targets.automations || []).map(code => `automation:${code}`),
1751
+ ].sort();
1752
+ const backend = byKind('BackendRelease');
1753
+ if (expectedBackendSelectors.length > 0) {
1754
+ if (
1755
+ backend.length !== 1 ||
1756
+ !exactStringSet(
1757
+ backend[0]?.metadata?.handledSelectors,
1758
+ expectedBackendSelectors
1759
+ )
1760
+ ) {
1761
+ fail(
1762
+ `APP_RELEASE_STAGED_SCOPE_MISMATCH: Backend handledSelectors 必须精确为 ${expectedBackendSelectors.join(',')}`
1763
+ );
1764
+ }
1765
+ } else if (backend.length > 0) {
1766
+ fail('APP_RELEASE_STAGED_SCOPE_MISMATCH: SDD 未授权 BackendRelease child');
1767
+ }
1768
+
1769
+ const expectedForms = (targets.forms || []).map(String).sort();
1770
+ const actualForms = byKind('FormRelease')
1771
+ .map(resource => stagedFormCode(target, resource))
1772
+ .sort();
1773
+ if (!exactStringSet(actualForms, expectedForms)) {
1774
+ fail(
1775
+ `APP_RELEASE_STAGED_SCOPE_MISMATCH: FormRelease 必须精确覆盖 ${expectedForms.join(',') || '(none)'}`
1776
+ );
1777
+ }
1778
+
1779
+ const runtimeCount = byKind('RuntimeRelease').length;
1780
+ if (runtimeCount !== (targets.runtime ? 1 : 0)) {
1781
+ fail(
1782
+ `APP_RELEASE_STAGED_SCOPE_MISMATCH: RuntimeRelease 期望 ${targets.runtime ? 1 : 0} 个,实际 ${runtimeCount} 个`
1783
+ );
1784
+ }
1785
+ const pageCount = byKind('PageRelease').length;
1786
+ const expectsPageRelease = (targets.pages || []).length > 0;
1787
+ if (pageCount !== (expectsPageRelease ? 1 : 0)) {
1788
+ fail(
1789
+ `APP_RELEASE_STAGED_SCOPE_MISMATCH: PageRelease 期望 ${expectsPageRelease ? 1 : 0} 个,实际 ${pageCount} 个`
1790
+ );
1791
+ }
1792
+ return { changeId, scope, resources: normalized };
1793
+ }
1794
+
1795
+ function changedAppReleaseResources(detailResources, captureResources) {
1796
+ const captured = new Map(
1797
+ (captureResources || []).map((resource, index) => {
1798
+ const described = describeAppReleaseResource(
1799
+ resource,
1800
+ index,
1801
+ 'capture.resources'
1802
+ );
1803
+ return [described.stableKey, described];
1804
+ })
1805
+ );
1806
+ return (detailResources || []).filter((resource, index) => {
1807
+ const described = describeAppReleaseResource(
1808
+ resource,
1809
+ index,
1810
+ 'release.resources'
1811
+ );
1812
+ const active = captured.get(described.stableKey);
1813
+ return (
1814
+ !active ||
1815
+ active.releaseId !== described.releaseId ||
1816
+ active.hash !== described.hash
1817
+ );
1818
+ });
1819
+ }
1820
+
1821
+ function assertAppReleaseActualGate(target, flags, scope, command) {
1822
+ assertSddReleaseGate(
1823
+ buildExplicitSddReleasePlan(scope.targets, {
1824
+ files: scope.changedFiles || [],
1825
+ commands: [command],
1826
+ changeId: scope.changeId,
1827
+ actualCommands: true,
1828
+ }),
1829
+ flags,
1830
+ command
1831
+ );
1832
+ }
1833
+
1565
1834
  function stagedAppReleaseProtocolVersion(stagedResources) {
1566
1835
  const hasBackendReleaseV2 = (stagedResources || []).some(
1567
1836
  resource =>
@@ -1767,6 +2036,9 @@ async function runAppReleaseCommand(
1767
2036
  );
1768
2037
  } else if (subcommand === 'app-prepare') {
1769
2038
  const stagedResources = readStagedAppReleaseResources(flags);
2039
+ if (stagedResources) {
2040
+ assertStagedAppReleaseScope(target, flags, stagedResources);
2041
+ }
1770
2042
  const protocolVersion = stagedAppReleaseProtocolVersion(stagedResources);
1771
2043
  if (protocolVersion) {
1772
2044
  assertAppFunctionSecretCapabilities(
@@ -1803,6 +2075,15 @@ async function runAppReleaseCommand(
1803
2075
  target.profileName,
1804
2076
  appReleaseApiPath(target, `/${encodeURIComponent(releaseId)}`)
1805
2077
  );
2078
+ const detailResources = appReleaseManifestResources(detail);
2079
+ const capture = await fetchAppReleaseCapture(config, target, flags);
2080
+ const changedResources = changedAppReleaseResources(
2081
+ detailResources,
2082
+ capture.resources
2083
+ );
2084
+ if (changedResources.length > 0) {
2085
+ assertStagedAppReleaseScope(target, flags, changedResources);
2086
+ }
1806
2087
  const protocolVersion = String(detail?.protocolVersion || '');
1807
2088
  if (protocolVersion === APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation) {
1808
2089
  assertAppFunctionSecretCapabilities(
@@ -1820,7 +2101,7 @@ async function runAppReleaseCommand(
1820
2101
  target,
1821
2102
  releaseContext,
1822
2103
  detail,
1823
- appReleaseManifestResources(detail),
2104
+ detailResources,
1824
2105
  { protocolVersion: protocolVersion || undefined }
1825
2106
  );
1826
2107
  } else if (subcommand === 'app-activate') {
@@ -1835,6 +2116,18 @@ async function runAppReleaseCommand(
1835
2116
  const protocolVersion = String(detail?.protocolVersion || '');
1836
2117
  const atomicV2 =
1837
2118
  protocolVersion === APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation;
2119
+ const activatesStagedChildren =
2120
+ atomicV2 ||
2121
+ flags['activate-staged-children'] === true ||
2122
+ String(flags['activate-staged-children'] || '').toLowerCase() === 'true';
2123
+ if (activatesStagedChildren) {
2124
+ const capture = await fetchAppReleaseCapture(config, target, flags);
2125
+ const changedResources = changedAppReleaseResources(
2126
+ appReleaseManifestResources(detail),
2127
+ capture.resources
2128
+ );
2129
+ assertStagedAppReleaseScope(target, flags, changedResources);
2130
+ }
1838
2131
  if (atomicV2) {
1839
2132
  assertAppFunctionSecretCapabilities(
1840
2133
  await fetchAppFunctionSecretCapabilities(config, target)
@@ -1853,15 +2146,30 @@ async function runAppReleaseCommand(
1853
2146
  releaseId,
1854
2147
  {
1855
2148
  activateStagedChildren:
1856
- atomicV2 ||
1857
- flags['activate-staged-children'] === true ||
1858
- String(flags['activate-staged-children'] || '').toLowerCase() ===
1859
- 'true',
2149
+ activatesStagedChildren,
1860
2150
  protocolVersion: protocolVersion || undefined,
1861
2151
  }
1862
2152
  );
1863
2153
  } else if (subcommand === 'app-finalize') {
1864
2154
  const stagedResources = readStagedAppReleaseResources(flags);
2155
+ let stagedScope = null;
2156
+ if (stagedResources) {
2157
+ stagedScope = assertStagedAppReleaseScope(
2158
+ target,
2159
+ flags,
2160
+ stagedResources
2161
+ );
2162
+ if (stagedScope.scope) {
2163
+ assertAppReleaseActualGate(
2164
+ target,
2165
+ flags,
2166
+ stagedScope.scope,
2167
+ `openxiangda release app-finalize --staged-resources-json ${String(
2168
+ flags['staged-resources-json']
2169
+ )} --profile ${target.profileName} --change ${stagedScope.changeId}`
2170
+ );
2171
+ }
2172
+ }
1865
2173
  const protocolVersion = stagedAppReleaseProtocolVersion(stagedResources);
1866
2174
  if (protocolVersion) {
1867
2175
  assertAppFunctionSecretCapabilities(
@@ -1914,6 +2222,16 @@ async function runAppReleaseCommand(
1914
2222
  protocolVersion,
1915
2223
  }
1916
2224
  );
2225
+ let stateReconcileWarning = null;
2226
+ try {
2227
+ const appHead = await loadRuntimeHeadApp(config, target);
2228
+ saveRuntimeHeadState(target, appHead);
2229
+ } catch (error) {
2230
+ stateReconcileWarning = {
2231
+ component: 'RuntimeStateReadback',
2232
+ message: error?.message || String(error),
2233
+ };
2234
+ }
1917
2235
  data = {
1918
2236
  activationModel: stagedResources
1919
2237
  ? protocolVersion || 'atomic_staged_children_v1'
@@ -1923,6 +2241,7 @@ async function runAppReleaseCommand(
1923
2241
  prepared,
1924
2242
  verified,
1925
2243
  activated,
2244
+ ...(stateReconcileWarning ? { stateReconcileWarning } : {}),
1926
2245
  };
1927
2246
  } else if (subcommand === 'app-rollback') {
1928
2247
  if (!releaseId) {
@@ -3962,7 +4281,19 @@ async function workspace(args) {
3962
4281
  if (publishOptions.dryRun) {
3963
4282
  printWorkspaceReleasePlan(sddPlan);
3964
4283
  } else {
3965
- assertSddReleaseGate(sddPlan, flags, 'openxiangda workspace publish');
4284
+ const actualSddPlan = buildExplicitSddReleasePlan(sddPlan.targets, {
4285
+ files: sddPlan.changedFiles || [],
4286
+ commands: [
4287
+ buildWorkspacePublishCommand(flags, profileName),
4288
+ ],
4289
+ changeId: publishOptions.changeId,
4290
+ actualCommands: true,
4291
+ });
4292
+ assertSddReleaseGate(
4293
+ actualSddPlan,
4294
+ flags,
4295
+ 'openxiangda workspace publish'
4296
+ );
3966
4297
  }
3967
4298
  await requestWithAuth(
3968
4299
  config,
@@ -6341,6 +6672,7 @@ const SECRET_RESPONSE_FORBIDDEN_KEYS = new Set([
6341
6672
  'cipher',
6342
6673
  'encryptedvalue',
6343
6674
  'encrypteddek',
6675
+ 'wrappeddek',
6344
6676
  'valuehash',
6345
6677
  'cipherhash',
6346
6678
  'valuedigest',
@@ -6350,6 +6682,7 @@ const SECRET_RESPONSE_FORBIDDEN_KEYS = new Set([
6350
6682
  'plaintextlength',
6351
6683
  'iv',
6352
6684
  'nonce',
6685
+ 'authtag',
6353
6686
  'tag',
6354
6687
  ]);
6355
6688
 
@@ -8477,8 +8810,16 @@ async function resource(args) {
8477
8810
  resourceTargets,
8478
8811
  {
8479
8812
  files: scopedFiles,
8480
- commands: [buildResourcePublishCommand(typeFilters, codeFilters, profileName)],
8813
+ commands: [
8814
+ buildResourcePublishCommand(
8815
+ typeFilters,
8816
+ codeFilters,
8817
+ profileName,
8818
+ flags
8819
+ ),
8820
+ ],
8481
8821
  changeId: flags.change,
8822
+ actualCommands: true,
8482
8823
  }
8483
8824
  ),
8484
8825
  flags,
@@ -8528,6 +8869,21 @@ async function resource(args) {
8528
8869
  if (changeBaselinePreflight) {
8529
8870
  result.changeBaselinePreflight = changeBaselinePreflight;
8530
8871
  }
8872
+ const stagedResources =
8873
+ Array.isArray(result.stagedResources) && result.stagedResources.length > 0
8874
+ ? result.stagedResources
8875
+ : result.stagedResource
8876
+ ? [result.stagedResource]
8877
+ : [];
8878
+ const stagedChangeId =
8879
+ flags.change || getStoredChangeBaseline(target)?.changeId;
8880
+ if (stagedResources.length > 0 && stagedChangeId) {
8881
+ result.stagedResourcesFile = recordStagedAppReleaseResources(
8882
+ stagedChangeId,
8883
+ stagedResources,
8884
+ target
8885
+ );
8886
+ }
8531
8887
  result.timings = telemetry.stop();
8532
8888
  if (flags.json) return writeJson(result);
8533
8889
  printResourceResult(result);
@@ -8584,7 +8940,12 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
8584
8940
  .map(action => action.code)
8585
8941
  .filter(Boolean)
8586
8942
  ).sort();
8587
- const codeResourceKeys = new Set(['functions', 'automations', 'workflows']);
8943
+ const codeResourceKeys = new Set([
8944
+ 'functions',
8945
+ 'automations',
8946
+ 'workflows',
8947
+ 'formSettings',
8948
+ ]);
8588
8949
  const hasGenericResources = RESOURCE_SPECS.some(
8589
8950
  spec => !codeResourceKeys.has(spec.key) && (manifest[spec.key] || []).length > 0
8590
8951
  ) || (planActions || []).some(
@@ -8593,7 +8954,7 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
8593
8954
  !['function', 'automation', 'workflow'].includes(action.kind)
8594
8955
  );
8595
8956
  return {
8596
- forms: [],
8957
+ forms: codes('formSettings'),
8597
8958
  pages: [],
8598
8959
  functions: unique([...codes('functions'), ...deletedCodes('function')]).sort(),
8599
8960
  automations: unique([
@@ -8608,20 +8969,51 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
8608
8969
  };
8609
8970
  }
8610
8971
 
8611
- function buildResourcePublishCommand(typeFilters, codeFilters, profileName) {
8972
+ function buildResourcePublishCommand(
8973
+ typeFilters,
8974
+ codeFilters,
8975
+ profileName,
8976
+ flags = {}
8977
+ ) {
8612
8978
  const args = ['openxiangda resource publish'];
8613
- if (typeFilters?.size === 1) {
8614
- args.push(Array.from(typeFilters)[0]);
8979
+ if (typeFilters?.size > 0) {
8980
+ args.push(Array.from(typeFilters).sort().join(','));
8615
8981
  }
8616
8982
  if (codeFilters?.length) {
8983
+ const qualifierAliases = {
8984
+ functions: 'function',
8985
+ automations: 'automation',
8986
+ workflows: 'workflow',
8987
+ formSettings: 'form-setting',
8988
+ };
8617
8989
  const selectors = codeFilters.map(selector =>
8618
8990
  (selector.key || selector.qualifier) && (!typeFilters || typeFilters.size !== 1)
8619
- ? `${selector.key || selector.qualifier}:${selector.code}`
8991
+ ? `${
8992
+ qualifierAliases[selector.key || selector.qualifier] ||
8993
+ selector.key ||
8994
+ selector.qualifier
8995
+ }:${selector.code}`
8620
8996
  : selector.code
8621
8997
  );
8622
8998
  args.push(`--only ${Array.from(new Set(selectors)).join(',')}`);
8623
8999
  }
8624
9000
  args.push(`--profile ${profileName}`);
9001
+ if (flags.change) args.push(`--change ${flags.change}`);
9002
+ if (flags['stage-only']) args.push('--stage-only');
9003
+ if (flags.activate) args.push('--activate');
9004
+ if (flags['replace-manifest']) args.push('--replace-manifest');
9005
+ if (flags.reason) args.push(`--reason ${JSON.stringify(String(flags.reason))}`);
9006
+ return args.join(' ');
9007
+ }
9008
+
9009
+ function buildWorkspacePublishCommand(flags = {}, profileName) {
9010
+ const args = ['openxiangda workspace publish', `--profile ${profileName}`];
9011
+ if (flags.change) args.push(`--change ${flags.change}`);
9012
+ if (flags.form) args.push(`--form ${flags.form}`);
9013
+ if (flags.page) args.push(`--page ${flags.page}`);
9014
+ if (flags.only) args.push(`--only ${flags.only}`);
9015
+ if (flags.changed) args.push('--changed');
9016
+ if (flags['include-resources']) args.push('--include-resources');
8625
9017
  return args.join(' ');
8626
9018
  }
8627
9019
 
@@ -8729,9 +9121,10 @@ async function runtime(args) {
8729
9121
  commands: [
8730
9122
  `openxiangda runtime deploy --profile ${target.profileName}${
8731
9123
  flags.change ? ` --change ${flags.change}` : ''
8732
- }`,
9124
+ }${flags['no-activate'] ? ' --no-activate' : ''}`,
8733
9125
  ],
8734
9126
  changeId: flags.change,
9127
+ actualCommands: true,
8735
9128
  }
8736
9129
  ),
8737
9130
  flags,
@@ -8968,7 +9361,46 @@ async function runtime(args) {
8968
9361
  }
8969
9362
  )
8970
9363
  );
8971
- saveRuntimeReleaseState(target, data);
9364
+ const runtimeContentHash = String(data?.contentHash || '')
9365
+ .trim()
9366
+ .toLowerCase();
9367
+ const stagedResource = flags['no-activate']
9368
+ ? stripUndefinedValues({
9369
+ kind: 'RuntimeRelease',
9370
+ identity: { releaseId: data?.id },
9371
+ action: 'update',
9372
+ hash: runtimeContentHash,
9373
+ revision: {
9374
+ parentReleaseId:
9375
+ data?.parentReleaseId || runtimeLineage.parentReleaseId || null,
9376
+ },
9377
+ metadata: {
9378
+ buildId: data?.buildId || buildId,
9379
+ releaseStatus: data?.status || 'staged',
9380
+ },
9381
+ })
9382
+ : null;
9383
+ if (
9384
+ flags['no-activate'] &&
9385
+ (!data?.id || !/^[a-f0-9]{64}$/.test(runtimeContentHash))
9386
+ ) {
9387
+ fail(
9388
+ 'RUNTIME_RELEASE_STAGED_RESOURCE_INVALID: --no-activate 响应缺少 releaseId 或 64 位 contentHash'
9389
+ );
9390
+ }
9391
+ if (!flags['no-activate']) {
9392
+ saveRuntimeReleaseState(target, data);
9393
+ }
9394
+ const stagedChangeId =
9395
+ flags.change || getStoredChangeBaseline(target)?.changeId;
9396
+ const stagedResourcesFile =
9397
+ stagedResource && stagedChangeId
9398
+ ? recordStagedAppReleaseResources(
9399
+ stagedChangeId,
9400
+ [stagedResource],
9401
+ target
9402
+ )
9403
+ : null;
8972
9404
  const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
8973
9405
  const result = {
8974
9406
  appType: target.appType,
@@ -8990,6 +9422,8 @@ async function runtime(args) {
8990
9422
  releaseFiles.find(file => file.storageProvider)?.storageProvider ||
8991
9423
  null,
8992
9424
  traceId,
9425
+ ...(stagedResource ? { stagedResource } : {}),
9426
+ ...(stagedResourcesFile ? { stagedResourcesFile } : {}),
8993
9427
  timings: telemetry.stop(),
8994
9428
  };
8995
9429
  if (flags.json) return writeJson(result);
@@ -9002,6 +9436,9 @@ async function runtime(args) {
9002
9436
  `traceId: ${traceId}`,
9003
9437
  `assetBase: ${releaseAssetBaseUrl}`,
9004
9438
  `active: ${result.activated ? 'yes' : 'no'}`,
9439
+ ...(stagedResourcesFile
9440
+ ? [`staged resources: ${stagedResourcesFile}`]
9441
+ : []),
9005
9442
  ].join('\n')
9006
9443
  );
9007
9444
  return;
@@ -9482,6 +9919,21 @@ function saveRuntimeReleaseState(target, release) {
9482
9919
  saveProjectState(target.state);
9483
9920
  }
9484
9921
 
9922
+ function saveRuntimeHeadState(target, app) {
9923
+ const activeReleaseId =
9924
+ app?.activeRuntimeReleaseId || app?.activeRuntime?.releaseId || null;
9925
+ const activeBuildId =
9926
+ app?.activeRuntimeBuildId || app?.activeRuntime?.buildId || null;
9927
+ if (!activeReleaseId && !activeBuildId) return false;
9928
+ saveRuntimeReleaseState(target, {
9929
+ id: activeReleaseId,
9930
+ buildId: activeBuildId,
9931
+ assetBaseUrl:
9932
+ app?.runtimeAssetBaseUrl || app?.activeRuntime?.assetBaseUrl || null,
9933
+ });
9934
+ return true;
9935
+ }
9936
+
9485
9937
  function formatBytes(value) {
9486
9938
  const bytes = Number(value || 0);
9487
9939
  if (bytes < 1024) return `${bytes}B`;
@@ -12926,22 +13378,18 @@ function collectBackendReleaseCandidates(manifest, options = {}) {
12926
13378
  const code = item.code || item.functionCode || item.resourceCode;
12927
13379
  const action = getPlannedResourceAction(options, planKind, code);
12928
13380
  if (!action || !['create', 'update', 'noop'].includes(action.action)) continue;
12929
- const secretRefsDeclared =
12930
- kind === 'Function' && functionManifestDeclaresSecretRefs(item);
12931
- if (action.action === 'create' && !secretRefsDeclared) continue;
12932
13381
  if (action.action === 'noop' && options.skipNoop !== false) continue;
12933
- if (
12934
- shouldReplaceResourceManifestItem(
12935
- item,
12936
- resourceKey,
12937
- code,
12938
- options
12939
- ) &&
12940
- !secretRefsDeclared
12941
- ) {
12942
- continue;
12943
- }
12944
- candidates.push({ kind, code, item, plannedAction: action.action });
13382
+ const replaceManifest =
13383
+ action.action !== 'create' &&
13384
+ shouldReplaceResourceManifestItem(item, resourceKey, code, options);
13385
+ candidates.push({
13386
+ kind,
13387
+ code,
13388
+ item,
13389
+ plannedAction: action.action,
13390
+ operation: action.action === 'create' ? 'create' : 'update',
13391
+ mode: replaceManifest ? 'manifest_replacement' : 'source_only',
13392
+ });
12945
13393
  }
12946
13394
  };
12947
13395
  append('Function', 'functions', manifest.functions);
@@ -13116,6 +13564,28 @@ async function buildBackendReleaseResourceInput(
13116
13564
  const secretRefsDeclared = functionManifestDeclaresSecretRefs(
13117
13565
  candidate.item
13118
13566
  );
13567
+ const common = {
13568
+ kind: 'Function',
13569
+ code: candidate.code,
13570
+ mode: candidate.mode,
13571
+ operation: candidate.operation,
13572
+ ...(secretRefsDeclared
13573
+ ? {
13574
+ secretRefs: normalizeFunctionSecretRefs(
13575
+ candidate.item.secretRefs
13576
+ ),
13577
+ }
13578
+ : {}),
13579
+ };
13580
+ if (candidate.mode === 'manifest_replacement') {
13581
+ return {
13582
+ ...common,
13583
+ replacement: buildBackendReleaseFunctionReplacementInput(
13584
+ candidate.item,
13585
+ definitionJson
13586
+ ),
13587
+ };
13588
+ }
13119
13589
  const source = backendSourcePlane(
13120
13590
  definitionJson,
13121
13591
  `Function:${candidate.code}`
@@ -13124,36 +13594,71 @@ async function buildBackendReleaseResourceInput(
13124
13594
  source.version = definitionJson.version;
13125
13595
  source.runtimeContractVersion = definitionJson.runtimeContractVersion;
13126
13596
  }
13127
- const resource = {
13128
- kind: 'Function',
13129
- code: candidate.code,
13130
- ...(candidate.plannedAction === 'create'
13597
+ return {
13598
+ ...common,
13599
+ ...(candidate.operation === 'create'
13131
13600
  ? {
13132
- operation: 'create',
13133
13601
  create: buildBackendReleaseFunctionCreateInput(
13134
13602
  candidate.item,
13135
13603
  definitionJson
13136
13604
  ),
13137
13605
  }
13138
- : { operation: 'update' }),
13139
- source,
13140
- ...(secretRefsDeclared
13141
- ? {
13142
- secretRefs: normalizeFunctionSecretRefs(
13143
- candidate.item.secretRefs
13144
- ),
13145
- }
13146
13606
  : {}),
13607
+ source,
13147
13608
  };
13148
- return resource;
13149
13609
  }
13150
- return {
13610
+ const viewJson =
13611
+ (await resolveManifestJson(
13612
+ config,
13613
+ target.profileName,
13614
+ candidate.item,
13615
+ 'viewJson',
13616
+ 'viewFile',
13617
+ true,
13618
+ uploadCache
13619
+ )) ??
13620
+ (await resolveManifestJson(
13621
+ config,
13622
+ target.profileName,
13623
+ candidate.item,
13624
+ 'previewJson',
13625
+ 'previewFile',
13626
+ true,
13627
+ uploadCache
13628
+ ));
13629
+ const common = {
13151
13630
  kind: 'Automation',
13152
13631
  code: candidate.code,
13632
+ mode: candidate.mode,
13633
+ operation: candidate.operation,
13634
+ };
13635
+ if (candidate.mode === 'manifest_replacement') {
13636
+ return {
13637
+ ...common,
13638
+ replacement: buildBackendReleaseAutomationMetadata(
13639
+ target,
13640
+ candidate.item,
13641
+ definitionJson,
13642
+ viewJson
13643
+ ),
13644
+ };
13645
+ }
13646
+ return {
13647
+ ...common,
13153
13648
  sources: buildAutomationBackendReleaseSources(
13154
13649
  definitionJson,
13155
13650
  `Automation:${candidate.code}`
13156
13651
  ),
13652
+ ...(candidate.operation === 'create'
13653
+ ? {
13654
+ create: buildBackendReleaseAutomationMetadata(
13655
+ target,
13656
+ candidate.item,
13657
+ withoutBackendSourcePlanes(definitionJson),
13658
+ viewJson
13659
+ ),
13660
+ }
13661
+ : {}),
13157
13662
  };
13158
13663
  }
13159
13664
 
@@ -13187,6 +13692,75 @@ function buildBackendReleaseFunctionCreateInput(functionItem, definitionJson) {
13187
13692
  });
13188
13693
  }
13189
13694
 
13695
+ function buildBackendReleaseFunctionReplacementInput(
13696
+ functionItem,
13697
+ definitionJson
13698
+ ) {
13699
+ const code =
13700
+ functionItem.code || functionItem.functionCode || functionItem.resourceCode;
13701
+ return stripUndefinedValues({
13702
+ name: functionItem.name || definitionJson.name || code,
13703
+ description:
13704
+ functionItem.description !== undefined
13705
+ ? functionItem.description
13706
+ : definitionJson.description || '',
13707
+ definitionJson: clonePlainJson(definitionJson),
13708
+ resourceBindings: clonePlainJson(definitionJson.resourceBindings || {}),
13709
+ inputSchema: clonePlainJson(
13710
+ functionItem.inputSchema || definitionJson.inputSchema
13711
+ ),
13712
+ outputSchema: clonePlainJson(
13713
+ functionItem.outputSchema || definitionJson.outputSchema
13714
+ ),
13715
+ status: functionItem.status || 'active',
13716
+ });
13717
+ }
13718
+
13719
+ function buildBackendReleaseAutomationMetadata(
13720
+ target,
13721
+ automationItem,
13722
+ definitionJson,
13723
+ viewJson
13724
+ ) {
13725
+ const payload = resolveAutomationManifestPayload(target, automationItem);
13726
+ const enabled =
13727
+ automationItem.enable !== undefined
13728
+ ? Boolean(automationItem.enable)
13729
+ : automationItem.isEnabled !== undefined
13730
+ ? Boolean(automationItem.isEnabled)
13731
+ : true;
13732
+ return stripUndefinedValues({
13733
+ name: automationItem.name || automationItem.code,
13734
+ description: automationItem.description || '',
13735
+ formUuid: payload.formUuid,
13736
+ triggerConfig: clonePlainJson(payload.triggerConfig),
13737
+ definitionJson: clonePlainJson(definitionJson),
13738
+ ...(viewJson !== undefined ? { viewJson: clonePlainJson(viewJson) } : {}),
13739
+ isPublished: Boolean(automationItem.publish || automationItem.isPublished),
13740
+ isEnabled: enabled,
13741
+ ...(automationItem.tags !== undefined
13742
+ ? {
13743
+ tags: Array.isArray(automationItem.tags)
13744
+ ? automationItem.tags.join(',')
13745
+ : automationItem.tags,
13746
+ }
13747
+ : {}),
13748
+ });
13749
+ }
13750
+
13751
+ function withoutBackendSourcePlanes(value) {
13752
+ if (Array.isArray(value)) return value.map(withoutBackendSourcePlanes);
13753
+ if (!value || typeof value !== 'object') return value;
13754
+ return Object.fromEntries(
13755
+ Object.entries(value)
13756
+ .filter(
13757
+ ([key]) =>
13758
+ !['sourceFile', 'sourceType', 'sourceProvenance'].includes(key)
13759
+ )
13760
+ .map(([key, item]) => [key, withoutBackendSourcePlanes(item)])
13761
+ );
13762
+ }
13763
+
13190
13764
  function indexCurrentAutomationStates(items, codes) {
13191
13765
  const codeSet = new Set(codes);
13192
13766
  const groups = new Map();
@@ -13273,6 +13847,9 @@ async function applyBackendAutomationStateTransitions(
13273
13847
  if (!current?.id) {
13274
13848
  fail(`Backend Release 激活后缺少 Automation:${candidate.code} 当前版本`);
13275
13849
  }
13850
+ if (candidate.mode === 'manifest_replacement') {
13851
+ return { code: candidate.code, transitions: [] };
13852
+ }
13276
13853
  const plan = getPlannedResourceAction(
13277
13854
  options,
13278
13855
  'automation',
@@ -13389,7 +13966,8 @@ function saveBackendReleaseResourceStates(
13389
13966
  function backendReleaseResultSummary(
13390
13967
  release,
13391
13968
  stateTransitions = new Map(),
13392
- protocolVersion = null
13969
+ protocolVersion = null,
13970
+ handledSelectors = []
13393
13971
  ) {
13394
13972
  const resources = release.resources || [];
13395
13973
  const resolvedProtocolVersion =
@@ -13400,8 +13978,17 @@ function backendReleaseResultSummary(
13400
13978
  action: 'update',
13401
13979
  hash: release.verificationHash || release.manifestHash,
13402
13980
  metadata: resolvedProtocolVersion
13403
- ? { protocolVersion: resolvedProtocolVersion }
13404
- : {},
13981
+ ? {
13982
+ protocolVersion: resolvedProtocolVersion,
13983
+ mode: release.mode,
13984
+ parentReleaseId: release.parentReleaseId || null,
13985
+ handledSelectors,
13986
+ }
13987
+ : {
13988
+ mode: release.mode,
13989
+ parentReleaseId: release.parentReleaseId || null,
13990
+ handledSelectors,
13991
+ },
13405
13992
  };
13406
13993
  const isStaged = ['prepared', 'verified'].includes(
13407
13994
  String(release.status || '').toLowerCase()
@@ -13412,6 +13999,8 @@ function backendReleaseResultSummary(
13412
13999
  manifestHash: release.manifestHash,
13413
14000
  verificationHash: release.verificationHash,
13414
14001
  parentReleaseId: release.parentReleaseId || null,
14002
+ mode: release.mode,
14003
+ handledSelectors,
13415
14004
  resourceCount: resources.length,
13416
14005
  updateCount: resources.filter(resource => resource.action === 'update').length,
13417
14006
  noopCount: resources.filter(resource => resource.action === 'noop').length,
@@ -13444,13 +14033,20 @@ async function publishBackendReleaseResources(
13444
14033
  }
13445
14034
  return;
13446
14035
  }
13447
- const protocolVersion = candidates.some(
13448
- candidate =>
13449
- candidate.kind === 'Function' &&
13450
- functionManifestDeclaresSecretRefs(candidate.item)
13451
- )
13452
- ? APP_FUNCTION_SECRET_CAPABILITIES.backendRelease
13453
- : null;
14036
+ const protocolVersion = APP_FUNCTION_SECRET_CAPABILITIES.backendRelease;
14037
+ const handledSelectors = candidates
14038
+ .map(candidate =>
14039
+ `${candidate.kind === 'Function' ? 'function' : 'automation'}:${candidate.code}`
14040
+ )
14041
+ .sort();
14042
+ const resourceModes = unique(candidates.map(candidate => candidate.mode));
14043
+ const releaseMode =
14044
+ resourceModes.length > 1 ? 'mixed' : resourceModes[0] || 'source_only';
14045
+ const replacementReason = String(
14046
+ options.publishLeaseFlags?.reason ||
14047
+ options.publishLeaseFlags?.['replace-reason'] ||
14048
+ ''
14049
+ ).trim();
13454
14050
 
13455
14051
  let head;
13456
14052
  try {
@@ -13460,19 +14056,26 @@ async function publishBackendReleaseResources(
13460
14056
  backendReleaseApiPath(target, '/head')
13461
14057
  );
13462
14058
  } catch (error) {
13463
- if (protocolVersion && isBackendReleaseFeatureAbsent(error)) {
14059
+ if (isBackendReleaseFeatureAbsent(error)) {
14060
+ const secretRefsRequired = candidates.some(
14061
+ candidate =>
14062
+ candidate.kind === 'Function' &&
14063
+ functionManifestDeclaresSecretRefs(candidate.item)
14064
+ );
14065
+ if (!options.stageBackendReleaseOnly && !secretRefsRequired) {
14066
+ const message =
14067
+ '当前平台未提供 Backend Release API(head=404);非 stage-only 发布兼容降级为逐项写入。升级平台后会自动使用 Backend Release v2。';
14068
+ result.warnings.push(message);
14069
+ warn(message);
14070
+ return;
14071
+ }
13464
14072
  const missing = new Error(
13465
- 'APP_FUNCTION_SECRET_CAPABILITY_REQUIRED: 带 secretRefs 的 Function 禁止降级为逐项 PATCH;平台必须支持 Backend Release v2'
14073
+ 'BACKEND_RELEASE_V2_REQUIRED: 本次 Function/Automation 发布包含 v2 operation/mode,平台必须支持 Backend Release v2,禁止降级为逐项写入'
13466
14074
  );
13467
- missing.code = 'APP_FUNCTION_SECRET_CAPABILITY_REQUIRED';
14075
+ missing.code = 'BACKEND_RELEASE_V2_REQUIRED';
13468
14076
  throw missing;
13469
14077
  }
13470
- if (!isBackendReleaseFeatureAbsent(error)) throw error;
13471
- const message =
13472
- '当前平台未提供 Backend Release API(head=404);本次兼容降级为逐项 source PATCH。升级平台后会自动启用 Function + Automation 单事务发布。';
13473
- result.warnings.push(message);
13474
- warn(message);
13475
- return;
14078
+ throw error;
13476
14079
  }
13477
14080
 
13478
14081
  const releaseContext = await ensureBackendReleasePublishContext(
@@ -13506,7 +14109,10 @@ async function publishBackendReleaseResources(
13506
14109
  ...releaseContext.control,
13507
14110
  ...(protocolVersion ? { protocolVersion } : {}),
13508
14111
  parentReleaseId: head?.activeBackendReleaseId || null,
13509
- mode: 'source_only',
14112
+ mode: releaseMode,
14113
+ ...(releaseMode !== 'source_only'
14114
+ ? { replacementReason }
14115
+ : {}),
13510
14116
  sourceRepositoryId: releaseContext.sourceRepositoryId,
13511
14117
  sourceRevision: releaseContext.sourceRevision,
13512
14118
  resources,
@@ -13575,11 +14181,14 @@ async function publishBackendReleaseResources(
13575
14181
  result.backendRelease = backendReleaseResultSummary(
13576
14182
  stagedRelease,
13577
14183
  new Map(),
13578
- protocolVersion
14184
+ protocolVersion,
14185
+ handledSelectors
13579
14186
  );
13580
14187
  if (!result.backendRelease.stagedResource) {
13581
14188
  fail('Backend Release stage-only 响应缺少 verified stagedResource');
13582
14189
  }
14190
+ result.stagedResource = result.backendRelease.stagedResource;
14191
+ result.handledSelectors = handledSelectors;
13583
14192
  return;
13584
14193
  }
13585
14194
  const activated = await requestWithAuth(
@@ -13660,8 +14269,10 @@ async function publishBackendReleaseResources(
13660
14269
  result.backendRelease = backendReleaseResultSummary(
13661
14270
  activated,
13662
14271
  stateTransitions,
13663
- protocolVersion
14272
+ protocolVersion,
14273
+ handledSelectors
13664
14274
  );
14275
+ result.handledSelectors = handledSelectors;
13665
14276
  }
13666
14277
 
13667
14278
  async function publishResourceManifest(config, target, manifest, options = {}) {
@@ -14111,9 +14722,15 @@ async function runResourceChangeBaselinePreflight(
14111
14722
  const artifactCache = { ...(baseline.localBaseArtifactHashes || {}) };
14112
14723
  const missingArtifactTargets = targets.filter(targetItem => {
14113
14724
  const frozen = baseline.resourceHeads?.[targetItem.kind]?.[targetItem.code];
14725
+ const cached = artifactCache[
14726
+ resourceBaselineTargetKey(targetItem.kind, targetItem.code)
14727
+ ];
14728
+ const hasStableLineage =
14729
+ frozen?.fields?.sourceLineageHash &&
14730
+ frozen.fields.sourceLineageHash !== canonicalJsonSha256(null);
14114
14731
  return (
14115
14732
  frozen?.fields?.sourceArtifactHash &&
14116
- !artifactCache[resourceBaselineTargetKey(targetItem.kind, targetItem.code)]
14733
+ (!cached || (hasStableLineage && !cached.sourceHash))
14117
14734
  );
14118
14735
  });
14119
14736
  if (missingArtifactTargets.length > 0) {
@@ -14144,7 +14761,26 @@ async function runResourceChangeBaselinePreflight(
14144
14761
  const artifact = artifactCache[
14145
14762
  resourceBaselineTargetKey(targetItem.kind, targetItem.code)
14146
14763
  ];
14147
- if (artifact?.artifactHash && fields.sourceArtifactHash) {
14764
+ const hasStableLineage =
14765
+ fields.sourceLineageHash &&
14766
+ fields.sourceLineageHash !== canonicalJsonSha256(null);
14767
+ if (hasStableLineage) {
14768
+ if (!artifact?.sourceHash) {
14769
+ throwSourceBaseDiverged(
14770
+ `Git 基线构建未生成稳定源码血缘: ${targetItem.kind}:${targetItem.code}`,
14771
+ [targetItem]
14772
+ );
14773
+ }
14774
+ fields.sourceLineageHash =
14775
+ targetItem.kind === 'Automation'
14776
+ ? canonicalJsonSha256([
14777
+ {
14778
+ nodeId: '$definition',
14779
+ sha256: String(artifact.sourceHash).toLowerCase(),
14780
+ },
14781
+ ])
14782
+ : String(artifact.sourceHash).toLowerCase();
14783
+ } else if (artifact?.artifactHash && fields.sourceArtifactHash) {
14148
14784
  fields.sourceArtifactHash =
14149
14785
  targetItem.kind === 'Automation'
14150
14786
  ? canonicalJsonSha256([
@@ -15030,6 +15666,54 @@ async function publishFormSettingsResources(
15030
15666
  { frozen, artifactId }
15031
15667
  );
15032
15668
  saveResourceEntry(target, 'formSettings', code, { formUuid });
15669
+ const releaseId = data?.release?.id;
15670
+ const contentHash = String(
15671
+ data?.release?.contentHash || data?.contentHash || ''
15672
+ ).toLowerCase();
15673
+ const stagedResource =
15674
+ data?.staged && !data?.active && releaseId
15675
+ ? {
15676
+ kind: 'FormRelease',
15677
+ identity: { releaseId, formUuid },
15678
+ action: 'update',
15679
+ hash: contentHash,
15680
+ revision: stripUndefinedValues({
15681
+ parentReleaseId:
15682
+ data?.release?.parentReleaseId ||
15683
+ frozen.expectedParent?.releaseId ||
15684
+ null,
15685
+ baseRevision:
15686
+ data?.release?.baseRevision ?? frozen.expectedParent?.revision,
15687
+ }),
15688
+ metadata: {
15689
+ releaseStatus: data?.releaseStatus || 'staged',
15690
+ formCode: code,
15691
+ },
15692
+ }
15693
+ : null;
15694
+ if (
15695
+ !activate &&
15696
+ !data?.noop &&
15697
+ (!data?.staged || data?.active || !releaseId || !stagedResource)
15698
+ ) {
15699
+ fail(
15700
+ `FORM_RELEASE_STAGE_REQUIRED: ${code} 的非激活 bundle 响应必须返回 staged=true、active=false、releaseId 和 contentHash`
15701
+ );
15702
+ }
15703
+ if (stagedResource && !/^[a-f0-9]{64}$/.test(contentHash)) {
15704
+ fail(
15705
+ `FORM_RELEASE_STAGED_RESOURCE_INVALID: ${code} 的 staged FormRelease 缺少 64 位 contentHash`
15706
+ );
15707
+ }
15708
+ if (stagedResource) {
15709
+ result.stagedResources = result.stagedResources || [];
15710
+ result.stagedResources.push(stagedResource);
15711
+ if (result.stagedResources.length === 1) {
15712
+ result.stagedResource = stagedResource;
15713
+ } else {
15714
+ delete result.stagedResource;
15715
+ }
15716
+ }
15033
15717
  result.published.push({
15034
15718
  kind: 'formSetting',
15035
15719
  code,
@@ -15040,7 +15724,9 @@ async function publishFormSettingsResources(
15040
15724
  staged: Boolean(data?.staged),
15041
15725
  active: Boolean(data?.active),
15042
15726
  releaseStatus: data?.releaseStatus,
15043
- releaseId: data?.release?.id,
15727
+ releaseId,
15728
+ contentHash: contentHash || undefined,
15729
+ ...(stagedResource ? { stagedResource } : {}),
15044
15730
  });
15045
15731
  }
15046
15732
  }
@@ -19013,11 +19699,37 @@ function normalizeJsonForPlan(value, baseDir, keyName) {
19013
19699
  result.code = result.code === undefined ? '' : result.code;
19014
19700
  result.runtimeMode = result.runtimeMode || 'trusted_node';
19015
19701
  result.sourceType = result.sourceType || 'file_snapshot';
19702
+ const sourceProvenance = resolveLocalSourceProvenanceForPlan(
19703
+ value.sourceFile.localPath,
19704
+ baseDir
19705
+ );
19706
+ if (sourceProvenance) {
19707
+ result.sourceProvenance = {
19708
+ ...(result.sourceProvenance || {}),
19709
+ ...sourceProvenance,
19710
+ };
19711
+ }
19016
19712
  }
19017
19713
  if (result.code === '') delete result.code;
19018
19714
  return sortObjectForStableJson(result);
19019
19715
  }
19020
19716
 
19717
+ function resolveLocalSourceProvenanceForPlan(localPath, baseDir) {
19718
+ const rawLocalPath = String(localPath);
19719
+ const baseResolvedPath = path.resolve(baseDir || process.cwd(), rawLocalPath);
19720
+ const cwdResolvedPath = path.resolve(process.cwd(), rawLocalPath);
19721
+ const resolvedLocalPath = fs.existsSync(baseResolvedPath)
19722
+ ? baseResolvedPath
19723
+ : cwdResolvedPath;
19724
+ const sourceInfo = getJsCodeSourceInfoForPlan(resolvedLocalPath);
19725
+ if (!sourceInfo) return null;
19726
+ return readJsCodeSourceProvenance(
19727
+ sourceInfo.workspaceRoot,
19728
+ sourceInfo.sourceKind,
19729
+ sourceInfo.scriptCode
19730
+ );
19731
+ }
19732
+
19021
19733
  function normalizeLocalSourceFileForPlan(localPath, baseDir) {
19022
19734
  const rawLocalPath = String(localPath);
19023
19735
  const baseResolvedPath = path.resolve(baseDir || process.cwd(), rawLocalPath);
@@ -19239,7 +19951,7 @@ async function resolveJsCodeSnapshotFiles(config, profileName, value, baseDir, u
19239
19951
  typeof value.sourceFile === 'object' &&
19240
19952
  value.sourceFile.localPath
19241
19953
  ) {
19242
- const sourceFile = await uploadJsCodeSnapshotFile(
19954
+ const snapshot = await uploadJsCodeSnapshotFile(
19243
19955
  config,
19244
19956
  profileName,
19245
19957
  value.sourceFile.localPath,
@@ -19249,7 +19961,13 @@ async function resolveJsCodeSnapshotFiles(config, profileName, value, baseDir, u
19249
19961
  );
19250
19962
  value.runtimeMode = value.runtimeMode || 'trusted_node';
19251
19963
  value.sourceType = 'file_snapshot';
19252
- value.sourceFile = sourceFile;
19964
+ value.sourceFile = snapshot.sourceFile;
19965
+ if (snapshot.sourceProvenance) {
19966
+ value.sourceProvenance = {
19967
+ ...(value.sourceProvenance || {}),
19968
+ ...snapshot.sourceProvenance,
19969
+ };
19970
+ }
19253
19971
  value.code = value.code || '';
19254
19972
  }
19255
19973
 
@@ -19284,6 +20002,14 @@ async function uploadJsCodeSnapshotFile(
19284
20002
  if (uploadCache.has(cacheKey)) return await uploadCache.get(cacheKey);
19285
20003
 
19286
20004
  const buffer = fs.readFileSync(bundlePath);
20005
+ const sourceInfo = getJsCodeSourceInfoForPlan(resolvedLocalPath);
20006
+ const sourceProvenance = sourceInfo
20007
+ ? readJsCodeSourceProvenance(
20008
+ sourceInfo.workspaceRoot,
20009
+ sourceInfo.sourceKind,
20010
+ sourceInfo.scriptCode
20011
+ )
20012
+ : null;
19287
20013
  const pending = requestFormWithAuth(
19288
20014
  config,
19289
20015
  profileName,
@@ -19303,10 +20029,14 @@ async function uploadJsCodeSnapshotFile(
19303
20029
  originalName: uploaded.originalName,
19304
20030
  contentType: uploaded.contentType,
19305
20031
  };
19306
- uploadCache.set(cacheKey, sourceFile);
20032
+ const snapshot = {
20033
+ sourceFile,
20034
+ ...(sourceProvenance ? { sourceProvenance } : {}),
20035
+ };
20036
+ uploadCache.set(cacheKey, snapshot);
19307
20037
  // Progress belongs on stderr so `--json` keeps stdout machine-readable.
19308
20038
  warn(`已上传JS_CODE快照: ${path.relative(process.cwd(), bundlePath)} (${sourceFile.sha256})`);
19309
- return sourceFile;
20039
+ return snapshot;
19310
20040
  });
19311
20041
  uploadCache.set(cacheKey, pending);
19312
20042
  try {
@@ -19317,6 +20047,38 @@ async function uploadJsCodeSnapshotFile(
19317
20047
  }
19318
20048
  }
19319
20049
 
20050
+ function readJsCodeSourceProvenance(workspaceRoot, sourceKind, scriptCode) {
20051
+ const key = `${sourceKind}/${scriptCode}`;
20052
+ for (const filename of [
20053
+ 'build-cache.json',
20054
+ 'build-cache.cli-v4.json',
20055
+ 'build-cache.cli-v3.json',
20056
+ ]) {
20057
+ try {
20058
+ const cache = JSON.parse(
20059
+ fs.readFileSync(
20060
+ path.join(workspaceRoot, '.openxiangda', filename),
20061
+ 'utf8'
20062
+ )
20063
+ );
20064
+ const metadata = cache?.targets?.[key];
20065
+ const sourceHash = String(metadata?.sourceHash || '').toLowerCase();
20066
+ if (!/^[a-f0-9]{64}$/.test(sourceHash)) continue;
20067
+ return {
20068
+ contractVersion: 'source_lineage_v1',
20069
+ sourceHash,
20070
+ entryPath: `src/${sourceKind}/${scriptCode}/index.ts`,
20071
+ dependencies: Array.isArray(metadata.dependencies)
20072
+ ? metadata.dependencies.slice().sort()
20073
+ : [],
20074
+ };
20075
+ } catch {
20076
+ // Custom workspace builders are compatible but may not emit lineage.
20077
+ }
20078
+ }
20079
+ return null;
20080
+ }
20081
+
19320
20082
  async function resolveJsCodeBundlePath(localPath, scriptCode) {
19321
20083
  const extension = path.extname(localPath).toLowerCase();
19322
20084
  if (['.js', '.cjs', '.mjs'].includes(extension)) {
@@ -19378,10 +20140,10 @@ async function resolveJsCodeBundlePath(localPath, scriptCode) {
19378
20140
  }
19379
20141
 
19380
20142
  function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
19381
- return spawnSync('pnpm', ['build-js-code', '--script', resolvedScriptCode, '--source', sourceKind], {
19382
- cwd: workspaceRoot,
19383
- encoding: 'utf8',
19384
- });
20143
+ return runWorkspaceJsCodeBuildBatch(
20144
+ workspaceRoot,
20145
+ [{ sourceKind, scriptCode: resolvedScriptCode }]
20146
+ );
19385
20147
  }
19386
20148
 
19387
20149
  function isUnsupportedFunctionsBuildScript(result) {
@@ -19695,26 +20457,7 @@ function getChangedManifestResourceCodes(file, resourceKey, baseRef = 'HEAD') {
19695
20457
  }
19696
20458
 
19697
20459
  function shouldReplaceResourceManifestItem(item, resourceKey, code, options = {}) {
19698
- if (options.replaceManifest) return true;
19699
- if (!item?.__source) return false;
19700
- const relativeFile = normalizeWorkspaceRelativePath(
19701
- path.relative(process.cwd(), path.resolve(item.__source))
19702
- );
19703
- const scopedFiles = new Set(
19704
- (options.scopedFiles || []).map(normalizeWorkspaceRelativePath)
19705
- );
19706
- if (!scopedFiles.has(relativeFile)) return false;
19707
- const changedCodes = getChangedManifestResourceCodes(
19708
- relativeFile,
19709
- resourceKey,
19710
- options.baseRef || 'HEAD'
19711
- );
19712
- if (changedCodes.length === 0) {
19713
- // The SDD/change scope may refer to a committed manifest change that no
19714
- // longer differs from HEAD inside the release worktree.
19715
- return true;
19716
- }
19717
- return changedCodes.includes(String(code));
20460
+ return options.replaceManifest === true;
19718
20461
  }
19719
20462
 
19720
20463
  function buildAutomationSourcePatchSources(existingDefinition, desiredDefinition) {
@@ -20068,19 +20811,27 @@ function scanWorkspaceReliabilityIssues(files) {
20068
20811
 
20069
20812
  function buildTargetedResourceReleaseCommands(targets, profileArg, changeId) {
20070
20813
  const changeArg = changeId ? ` --change ${changeId}` : '';
20071
- if (targets.resources || (targets.jsCodeNodes || []).length > 0) {
20072
- return [`openxiangda resource publish --profile ${profileArg}${changeArg}`];
20073
- }
20074
20814
  const commands = [];
20075
- for (const [targetKey, resourceType] of [
20076
- ['functions', 'function'],
20077
- ['automations', 'automation'],
20078
- ['workflows', 'workflow'],
20079
- ]) {
20080
- const codes = targets[targetKey] || [];
20081
- if (codes.length === 0) continue;
20815
+ const functions = targets.functions || [];
20816
+ const automations = targets.automations || [];
20817
+ if (functions.length > 0 || automations.length > 0) {
20818
+ const resourceTypes = [
20819
+ ...(functions.length > 0 ? ['function'] : []),
20820
+ ...(automations.length > 0 ? ['automation'] : []),
20821
+ ];
20822
+ const qualify = resourceTypes.length > 1;
20823
+ const selectors = [
20824
+ ...functions.map(code => (qualify ? `function:${code}` : code)),
20825
+ ...automations.map(code => (qualify ? `automation:${code}` : code)),
20826
+ ];
20827
+ commands.push(
20828
+ `openxiangda resource publish ${resourceTypes.join(',')} --only ${selectors.join(',')} --stage-only --profile ${profileArg}${changeArg}`
20829
+ );
20830
+ }
20831
+ const workflows = targets.workflows || [];
20832
+ if (workflows.length > 0) {
20082
20833
  commands.push(
20083
- `openxiangda resource publish ${resourceType} --only ${codes.join(',')} --profile ${profileArg}${changeArg}`
20834
+ `openxiangda resource publish workflow --only ${workflows.join(',')} --profile ${profileArg}${changeArg}`
20084
20835
  );
20085
20836
  }
20086
20837
  return commands;
@@ -20096,9 +20847,9 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile, changeId)
20096
20847
  (targets.workflows || []).length > 0 ||
20097
20848
  (targets.jsCodeNodes || []).length > 0;
20098
20849
  if (runtimeMode === 'react-spa') {
20099
- for (const formCode of targets.forms) {
20850
+ if (targets.forms.length > 0) {
20100
20851
  commands.push(
20101
- `openxiangda workspace publish --profile ${profileArg} --form ${formCode}${changeId ? ` --change ${changeId}` : ''}`
20852
+ `openxiangda resource publish form-setting --only ${targets.forms.join(',')} --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
20102
20853
  );
20103
20854
  }
20104
20855
  if (hasResourceTargets) {
@@ -20106,7 +20857,17 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile, changeId)
20106
20857
  }
20107
20858
  if (targets.runtime) {
20108
20859
  commands.push(
20109
- `openxiangda runtime deploy --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
20860
+ `openxiangda runtime deploy --no-activate --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
20861
+ );
20862
+ }
20863
+ if (
20864
+ targets.forms.length > 0 ||
20865
+ (targets.functions || []).length > 0 ||
20866
+ (targets.automations || []).length > 0 ||
20867
+ targets.runtime
20868
+ ) {
20869
+ commands.push(
20870
+ `openxiangda release app-finalize --staged-resources-json .openxiangda/releases/${changeId || 'change'}/staged-resources.json --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
20110
20871
  );
20111
20872
  }
20112
20873
  return commands;
@@ -20170,12 +20931,35 @@ function buildWorkspaceReleasePlan(options = {}) {
20170
20931
  name: issue.rule,
20171
20932
  message: `${issue.file}:${issue.line} ${issue.message}`,
20172
20933
  }));
20173
- const commands = buildWorkspaceReleaseCommands(
20934
+ const generatedCommands = buildWorkspaceReleaseCommands(
20174
20935
  targets,
20175
20936
  runtimeMode,
20176
20937
  options.profile,
20177
20938
  changeScope?.changeId
20178
20939
  );
20940
+ const commands =
20941
+ changeScope?.commands?.length > 0
20942
+ ? [...changeScope.commands]
20943
+ : generatedCommands;
20944
+ if (!changeScope?.commands?.length && targets.resources) {
20945
+ errors.push({
20946
+ name: 'sdd-release-generic-resource-selector-required',
20947
+ message:
20948
+ '变更包含尚未归一化为 code 的通用资源;已保留可确定的 Function/Automation/Workflow 命令,请在 release.json.commands 中补一条精确 --only/--code 命令覆盖剩余资源。',
20949
+ });
20950
+ }
20951
+ if (
20952
+ !changeScope?.commands?.length &&
20953
+ (targets.jsCodeNodes || []).length > 0 &&
20954
+ (targets.functions || []).length === 0 &&
20955
+ (targets.automations || []).length === 0 &&
20956
+ (targets.workflows || []).length === 0
20957
+ ) {
20958
+ errors.push({
20959
+ name: 'sdd-release-js-code-owner-required',
20960
+ message: `JS_CODE 源码尚未映射到可发布资源: ${targets.jsCodeNodes.join(', ')};请在 coverage/release 中声明所属 Function、Automation 或 Workflow。`,
20961
+ });
20962
+ }
20179
20963
  const sdd = options.skipSdd
20180
20964
  ? null
20181
20965
  : buildSddGovernanceReport({
@@ -20290,6 +21074,7 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
20290
21074
  commands,
20291
21075
  changeId: options.changeId,
20292
21076
  verifyStage: options.verifyStage || 'prepublish',
21077
+ actualCommands: options.actualCommands === true,
20293
21078
  });
20294
21079
  return {
20295
21080
  targets,
@@ -20301,6 +21086,21 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
20301
21086
 
20302
21087
  function assertSddReleaseGate(plan, flags, command) {
20303
21088
  if (!plan?.sdd || plan.sdd.errors.length === 0) return;
21089
+ const nonBypassable = new Set([
21090
+ 'sdd-release-direct-form-schema-forbidden',
21091
+ 'sdd-release-direct-form-activation-forbidden',
21092
+ 'sdd-release-resource-scope-missing',
21093
+ 'sdd-release-runtime-direct-activation-forbidden',
21094
+ 'sdd-release-backend-direct-activation-forbidden',
21095
+ 'sdd-release-backend-scope-inexact',
21096
+ 'sdd-release-form-scope-inexact',
21097
+ ]);
21098
+ const hardFailure = plan.sdd.errors.find(error =>
21099
+ nonBypassable.has(error.name)
21100
+ );
21101
+ if (hardFailure) {
21102
+ fail(`${hardFailure.message}\n该原子发布安全门禁不可通过 --sdd-bypass 绕过。`);
21103
+ }
20304
21104
  if (flags['sdd-bypass']) {
20305
21105
  const bypass = appendBypassLog({
20306
21106
  cwd: process.cwd(),