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/README.md +7 -7
- package/lib/change-baseline.js +74 -6
- package/lib/cli.js +913 -113
- package/lib/sdd.js +320 -6
- package/openxiangda-skills/SKILL.md +10 -5
- package/openxiangda-skills/references/pages/publish-flow.md +15 -16
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +11 -7
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +10 -9
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +10 -10
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +20 -2
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -3
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +20 -2
package/lib/sdd.js
CHANGED
|
@@ -1223,6 +1223,12 @@ function getSddChangeScope(options = {}) {
|
|
|
1223
1223
|
...recordedChangedFiles,
|
|
1224
1224
|
...ownedFiles,
|
|
1225
1225
|
])).sort();
|
|
1226
|
+
const releaseCommands = Array.isArray(loaded.release?.commands)
|
|
1227
|
+
? loaded.release.commands.filter(command => String(command || '').trim())
|
|
1228
|
+
: [];
|
|
1229
|
+
const plannedCommands = Array.isArray(loaded.meta?.release?.plannedCommands)
|
|
1230
|
+
? loaded.meta.release.plannedCommands.filter(command => String(command || '').trim())
|
|
1231
|
+
: [];
|
|
1226
1232
|
return {
|
|
1227
1233
|
changeId: loaded.changeId,
|
|
1228
1234
|
change: loaded.meta,
|
|
@@ -1232,7 +1238,9 @@ function getSddChangeScope(options = {}) {
|
|
|
1232
1238
|
changedFiles,
|
|
1233
1239
|
recordedChangedFiles,
|
|
1234
1240
|
ownedFiles,
|
|
1235
|
-
commands:
|
|
1241
|
+
commands: releaseCommands.length > 0 ? releaseCommands : plannedCommands,
|
|
1242
|
+
releaseCommands,
|
|
1243
|
+
plannedCommands,
|
|
1236
1244
|
};
|
|
1237
1245
|
}
|
|
1238
1246
|
|
|
@@ -1382,6 +1390,7 @@ function buildSddGovernanceReport(options = {}) {
|
|
|
1382
1390
|
targets,
|
|
1383
1391
|
changedFiles: files,
|
|
1384
1392
|
commands: options.commands || [],
|
|
1393
|
+
actualCommands: options.actualCommands === true,
|
|
1385
1394
|
},
|
|
1386
1395
|
});
|
|
1387
1396
|
if (!verify.passed) {
|
|
@@ -1439,15 +1448,20 @@ function getSddVerifyStagePolicy(stageValue) {
|
|
|
1439
1448
|
if (stage === 'default') {
|
|
1440
1449
|
return {
|
|
1441
1450
|
stage,
|
|
1442
|
-
|
|
1443
|
-
|
|
1451
|
+
// An unspecified verify is a prepublish check. Postpublish/archive
|
|
1452
|
+
// evidence cannot exist yet and must never block ordinary development.
|
|
1453
|
+
taskStages: new Set(['implementation', 'prepublish']),
|
|
1454
|
+
evidenceStages: new Set(['prepublish']),
|
|
1444
1455
|
requireRelease: true,
|
|
1445
1456
|
};
|
|
1446
1457
|
}
|
|
1447
1458
|
const rank = SDD_VERIFY_STAGES.indexOf(stage);
|
|
1448
1459
|
return {
|
|
1449
1460
|
stage,
|
|
1450
|
-
taskStages:
|
|
1461
|
+
taskStages:
|
|
1462
|
+
stage === 'implementation'
|
|
1463
|
+
? new Set()
|
|
1464
|
+
: new Set(SDD_VERIFY_STAGES.slice(0, rank + 1)),
|
|
1451
1465
|
evidenceStages:
|
|
1452
1466
|
stage === 'implementation'
|
|
1453
1467
|
? new Set()
|
|
@@ -1661,13 +1675,19 @@ function validateDeltaSpecs(changeDir) {
|
|
|
1661
1675
|
|
|
1662
1676
|
function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'default') {
|
|
1663
1677
|
const verifiedAt = nowIso();
|
|
1678
|
+
const reviewedCommands = Array.isArray(loaded.release?.commands)
|
|
1679
|
+
? loaded.release.commands.filter(command => String(command || '').trim())
|
|
1680
|
+
: [];
|
|
1664
1681
|
return {
|
|
1665
1682
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1666
1683
|
changeId: loaded.changeId,
|
|
1667
1684
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
1668
1685
|
targets: releasePlan?.targets || loaded.release?.targets || {},
|
|
1669
1686
|
changedFiles: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
|
|
1670
|
-
commands:
|
|
1687
|
+
commands:
|
|
1688
|
+
reviewedCommands.length > 0
|
|
1689
|
+
? reviewedCommands
|
|
1690
|
+
: releasePlan?.commands || [],
|
|
1671
1691
|
verifiedAt,
|
|
1672
1692
|
verificationStage,
|
|
1673
1693
|
verificationStages: {
|
|
@@ -1769,9 +1789,270 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
|
|
|
1769
1789
|
if (releasePlan?.commands?.length && (!Array.isArray(release.commands) || release.commands.length === 0)) {
|
|
1770
1790
|
errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
|
|
1771
1791
|
}
|
|
1792
|
+
if (!legacyMode && Array.isArray(release.commands)) {
|
|
1793
|
+
errors.push(
|
|
1794
|
+
...validateAtomicReleaseCommands(
|
|
1795
|
+
release.commands,
|
|
1796
|
+
targets,
|
|
1797
|
+
release.changeId
|
|
1798
|
+
)
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1772
1801
|
return { errors, warnings };
|
|
1773
1802
|
}
|
|
1774
1803
|
|
|
1804
|
+
function commandFlagValue(command, name) {
|
|
1805
|
+
const match = String(command || '').match(
|
|
1806
|
+
new RegExp(`(?:^|\\s)--${escapeRegExp(name)}(?:=|\\s+)([^\\s]+)`)
|
|
1807
|
+
);
|
|
1808
|
+
return match ? match[1] : null;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function normalizedCommandSelectors(command) {
|
|
1812
|
+
return Array.from(
|
|
1813
|
+
new Set(
|
|
1814
|
+
String(commandFlagValue(command, 'only') || commandFlagValue(command, 'code') || '')
|
|
1815
|
+
.split(',')
|
|
1816
|
+
.map(value => value.trim())
|
|
1817
|
+
.filter(Boolean)
|
|
1818
|
+
)
|
|
1819
|
+
).sort();
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function normalizedResourcePublishTypes(command) {
|
|
1823
|
+
const match = String(command || '').match(
|
|
1824
|
+
/^openxiangda\s+resource\s+publish(?:\s+([^\s-][^\s]*))?/
|
|
1825
|
+
);
|
|
1826
|
+
if (!match?.[1]) return [];
|
|
1827
|
+
const aliases = {
|
|
1828
|
+
functions: 'function',
|
|
1829
|
+
automations: 'automation',
|
|
1830
|
+
workflows: 'workflow',
|
|
1831
|
+
'form-settings': 'form-setting',
|
|
1832
|
+
};
|
|
1833
|
+
return Array.from(
|
|
1834
|
+
new Set(
|
|
1835
|
+
match[1]
|
|
1836
|
+
.split(',')
|
|
1837
|
+
.map(value => value.trim().toLowerCase())
|
|
1838
|
+
.filter(Boolean)
|
|
1839
|
+
.map(value => aliases[value] || value)
|
|
1840
|
+
)
|
|
1841
|
+
).sort();
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
function sameStringSet(left, right) {
|
|
1845
|
+
const normalizedLeft = Array.from(new Set(left || [])).sort();
|
|
1846
|
+
const normalizedRight = Array.from(new Set(right || [])).sort();
|
|
1847
|
+
return (
|
|
1848
|
+
normalizedLeft.length === normalizedRight.length &&
|
|
1849
|
+
normalizedLeft.every((value, index) => value === normalizedRight[index])
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function validateAtomicReleaseCommands(
|
|
1854
|
+
commands,
|
|
1855
|
+
targets,
|
|
1856
|
+
changeId,
|
|
1857
|
+
options = {}
|
|
1858
|
+
) {
|
|
1859
|
+
const errors = [];
|
|
1860
|
+
const requireCompletePlan = options.requireCompletePlan !== false;
|
|
1861
|
+
const normalized = commands.map(command => String(command || '').trim());
|
|
1862
|
+
const publishCommands = normalized.filter(command =>
|
|
1863
|
+
/^openxiangda\s+(?:workspace\s+publish|resource\s+publish|runtime\s+deploy|release\s+app-finalize)\b/.test(
|
|
1864
|
+
command
|
|
1865
|
+
)
|
|
1866
|
+
);
|
|
1867
|
+
for (const command of publishCommands) {
|
|
1868
|
+
if (!/\s--profile(?:=|\s+)\S+/.test(command) || /<profile>/.test(command)) {
|
|
1869
|
+
errors.push({
|
|
1870
|
+
name: 'sdd-release-profile-missing',
|
|
1871
|
+
message: `发布命令必须显式指定真实 --profile: ${command}`,
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
if (
|
|
1875
|
+
changeId &&
|
|
1876
|
+
!new RegExp(`\\s--change(?:=|\\s+)${escapeRegExp(changeId)}(?:\\s|$)`).test(
|
|
1877
|
+
command
|
|
1878
|
+
)
|
|
1879
|
+
) {
|
|
1880
|
+
errors.push({
|
|
1881
|
+
name: 'sdd-release-change-missing',
|
|
1882
|
+
message: `发布命令必须绑定 --change ${changeId}: ${command}`,
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
if (/^openxiangda\s+workspace\s+publish\b/.test(command) && /\s--form(?:=|\s+)/.test(command)) {
|
|
1886
|
+
errors.push({
|
|
1887
|
+
name: 'sdd-release-direct-form-schema-forbidden',
|
|
1888
|
+
message: `React SPA Form 必须走 staged form-setting bundle,禁止 workspace publish --form: ${command}`,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
if (
|
|
1892
|
+
/^openxiangda\s+resource\s+publish\s+form-setting\b/.test(command) &&
|
|
1893
|
+
/\s--activate(?:\s|$)/.test(command)
|
|
1894
|
+
) {
|
|
1895
|
+
errors.push({
|
|
1896
|
+
name: 'sdd-release-direct-form-activation-forbidden',
|
|
1897
|
+
message: `Form bundle 必须保持 staged,禁止在 Root App finalize 前 --activate: ${command}`,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
if (
|
|
1901
|
+
/^openxiangda\s+resource\s+publish\b/.test(command) &&
|
|
1902
|
+
!/\s--(?:only|code)(?:=|\s+)/.test(command)
|
|
1903
|
+
) {
|
|
1904
|
+
errors.push({
|
|
1905
|
+
name: 'sdd-release-resource-scope-missing',
|
|
1906
|
+
message: `resource publish 必须使用 --only/--code 精确范围: ${command}`,
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
if (
|
|
1910
|
+
/^openxiangda\s+runtime\s+deploy\b/.test(command) &&
|
|
1911
|
+
!/\s--no-activate(?:\s|$)/.test(command)
|
|
1912
|
+
) {
|
|
1913
|
+
errors.push({
|
|
1914
|
+
name: 'sdd-release-runtime-direct-activation-forbidden',
|
|
1915
|
+
message: `Runtime 必须先 --no-activate 暂存,再由 App Release 原子激活: ${command}`,
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
const commandTypes = normalizedResourcePublishTypes(command);
|
|
1919
|
+
const expectedBackendTypes = [
|
|
1920
|
+
...((targets.functions || []).length > 0 ? ['function'] : []),
|
|
1921
|
+
...((targets.automations || []).length > 0 ? ['automation'] : []),
|
|
1922
|
+
].sort();
|
|
1923
|
+
if (
|
|
1924
|
+
commandTypes.some(type => ['function', 'automation'].includes(type)) &&
|
|
1925
|
+
expectedBackendTypes.length > 0
|
|
1926
|
+
) {
|
|
1927
|
+
const qualified = expectedBackendTypes.length > 1;
|
|
1928
|
+
const expectedSelectors = [
|
|
1929
|
+
...(targets.functions || []).map(code =>
|
|
1930
|
+
qualified ? `function:${code}` : String(code)
|
|
1931
|
+
),
|
|
1932
|
+
...(targets.automations || []).map(code =>
|
|
1933
|
+
qualified ? `automation:${code}` : String(code)
|
|
1934
|
+
),
|
|
1935
|
+
].sort();
|
|
1936
|
+
if (!/\s--stage-only(?:\s|$)/.test(command)) {
|
|
1937
|
+
errors.push({
|
|
1938
|
+
name: 'sdd-release-backend-direct-activation-forbidden',
|
|
1939
|
+
message: `Function/Automation 必须使用 --stage-only: ${command}`,
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
if (
|
|
1943
|
+
!sameStringSet(commandTypes, expectedBackendTypes) ||
|
|
1944
|
+
!sameStringSet(normalizedCommandSelectors(command), expectedSelectors)
|
|
1945
|
+
) {
|
|
1946
|
+
errors.push({
|
|
1947
|
+
name: 'sdd-release-backend-scope-inexact',
|
|
1948
|
+
message: `Function/Automation 命令必须精确覆盖审核 selector,不能缺失或夹带资源: ${command}`,
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (
|
|
1953
|
+
commandTypes.includes('form-setting') &&
|
|
1954
|
+
(targets.forms || []).length > 0 &&
|
|
1955
|
+
!sameStringSet(
|
|
1956
|
+
normalizedCommandSelectors(command),
|
|
1957
|
+
(targets.forms || []).map(String)
|
|
1958
|
+
)
|
|
1959
|
+
) {
|
|
1960
|
+
errors.push({
|
|
1961
|
+
name: 'sdd-release-form-scope-inexact',
|
|
1962
|
+
message: `Form bundle 命令必须精确覆盖审核 selector,不能缺失或夹带表单: ${command}`,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
if (!requireCompletePlan) return errors;
|
|
1968
|
+
|
|
1969
|
+
const backendCodes = [
|
|
1970
|
+
...(targets.functions || []),
|
|
1971
|
+
...(targets.automations || []),
|
|
1972
|
+
];
|
|
1973
|
+
if (backendCodes.length > 0) {
|
|
1974
|
+
const backendStage = normalized.find(
|
|
1975
|
+
command =>
|
|
1976
|
+
/^openxiangda\s+resource\s+publish\b/.test(command) &&
|
|
1977
|
+
/\s--stage-only(?:\s|$)/.test(command) &&
|
|
1978
|
+
sameStringSet(normalizedResourcePublishTypes(command), [
|
|
1979
|
+
...((targets.functions || []).length > 0 ? ['function'] : []),
|
|
1980
|
+
...((targets.automations || []).length > 0 ? ['automation'] : []),
|
|
1981
|
+
]) &&
|
|
1982
|
+
sameStringSet(normalizedCommandSelectors(command), [
|
|
1983
|
+
...(targets.functions || []).map(code =>
|
|
1984
|
+
(targets.automations || []).length > 0
|
|
1985
|
+
? `function:${code}`
|
|
1986
|
+
: String(code)
|
|
1987
|
+
),
|
|
1988
|
+
...(targets.automations || []).map(code =>
|
|
1989
|
+
(targets.functions || []).length > 0
|
|
1990
|
+
? `automation:${code}`
|
|
1991
|
+
: String(code)
|
|
1992
|
+
),
|
|
1993
|
+
])
|
|
1994
|
+
);
|
|
1995
|
+
if (!backendStage) {
|
|
1996
|
+
errors.push({
|
|
1997
|
+
name: 'sdd-release-backend-stage-missing',
|
|
1998
|
+
message: 'Function/Automation 必须由一条精确 --only 的 --stage-only Backend Release 命令覆盖。',
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
if ((targets.forms || []).length > 0) {
|
|
2003
|
+
const formStage = normalized.find(
|
|
2004
|
+
command =>
|
|
2005
|
+
/^openxiangda\s+resource\s+publish\s+form-setting\b/.test(command) &&
|
|
2006
|
+
!/\s--activate(?:\s|$)/.test(command) &&
|
|
2007
|
+
sameStringSet(
|
|
2008
|
+
normalizedCommandSelectors(command),
|
|
2009
|
+
(targets.forms || []).map(String)
|
|
2010
|
+
)
|
|
2011
|
+
);
|
|
2012
|
+
if (!formStage) {
|
|
2013
|
+
errors.push({
|
|
2014
|
+
name: 'sdd-release-form-stage-missing',
|
|
2015
|
+
message: 'Form 必须由精确 form-setting bundle 命令暂存为 FormRelease。',
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (targets.runtime) {
|
|
2020
|
+
const runtimeStage = normalized.find(
|
|
2021
|
+
command =>
|
|
2022
|
+
/^openxiangda\s+runtime\s+deploy\b/.test(command) &&
|
|
2023
|
+
/\s--no-activate(?:\s|$)/.test(command)
|
|
2024
|
+
);
|
|
2025
|
+
if (!runtimeStage) {
|
|
2026
|
+
errors.push({
|
|
2027
|
+
name: 'sdd-release-runtime-stage-missing',
|
|
2028
|
+
message: 'Runtime 目标缺少 runtime deploy --no-activate 暂存命令。',
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (
|
|
2033
|
+
backendCodes.length > 0 ||
|
|
2034
|
+
(targets.forms || []).length > 0 ||
|
|
2035
|
+
targets.runtime
|
|
2036
|
+
) {
|
|
2037
|
+
const finalize = normalized.find(
|
|
2038
|
+
command =>
|
|
2039
|
+
/^openxiangda\s+release\s+app-finalize\b/.test(command) &&
|
|
2040
|
+
/\s--staged-resources-json(?:=|\s+)\S+/.test(command)
|
|
2041
|
+
);
|
|
2042
|
+
if (!finalize) {
|
|
2043
|
+
errors.push({
|
|
2044
|
+
name: 'sdd-release-app-finalize-missing',
|
|
2045
|
+
message: '暂存的 Form/Backend/Runtime 必须由 app-finalize 一次原子激活。',
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return errors;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function escapeRegExp(value) {
|
|
2053
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1775
2056
|
function verifySddChange(options = {}) {
|
|
1776
2057
|
const loaded = loadSddChange(options);
|
|
1777
2058
|
const stagePolicy = getSddVerifyStagePolicy(options.stage || options.phase);
|
|
@@ -1833,6 +2114,35 @@ function verifySddChange(options = {}) {
|
|
|
1833
2114
|
const releaseValidation = validateReleaseManifest(release, options.releasePlan, legacyMode);
|
|
1834
2115
|
errors.push(...releaseValidation.errors);
|
|
1835
2116
|
warnings.push(...releaseValidation.warnings);
|
|
2117
|
+
const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
|
|
2118
|
+
if (
|
|
2119
|
+
!legacyMode &&
|
|
2120
|
+
Array.isArray(reviewedPlannedCommands) &&
|
|
2121
|
+
reviewedPlannedCommands.length > 0
|
|
2122
|
+
) {
|
|
2123
|
+
errors.push(
|
|
2124
|
+
...validateAtomicReleaseCommands(
|
|
2125
|
+
reviewedPlannedCommands,
|
|
2126
|
+
targets,
|
|
2127
|
+
loaded.changeId
|
|
2128
|
+
)
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (
|
|
2133
|
+
!legacyMode &&
|
|
2134
|
+
options.releasePlan?.actualCommands === true &&
|
|
2135
|
+
Array.isArray(options.releasePlan.commands) &&
|
|
2136
|
+
options.releasePlan.commands.length > 0
|
|
2137
|
+
) {
|
|
2138
|
+
errors.push(
|
|
2139
|
+
...validateAtomicReleaseCommands(
|
|
2140
|
+
options.releasePlan.commands,
|
|
2141
|
+
targets,
|
|
2142
|
+
loaded.changeId,
|
|
2143
|
+
{ requireCompletePlan: false }
|
|
2144
|
+
)
|
|
2145
|
+
);
|
|
1836
2146
|
}
|
|
1837
2147
|
const passed = errors.length === 0;
|
|
1838
2148
|
const verifiedAt = nowIso();
|
|
@@ -1840,7 +2150,11 @@ function verifySddChange(options = {}) {
|
|
|
1840
2150
|
...loaded.meta,
|
|
1841
2151
|
release: {
|
|
1842
2152
|
...(loaded.meta.release || {}),
|
|
1843
|
-
plannedCommands:
|
|
2153
|
+
plannedCommands:
|
|
2154
|
+
Array.isArray(loaded.meta.release?.plannedCommands) &&
|
|
2155
|
+
loaded.meta.release.plannedCommands.length > 0
|
|
2156
|
+
? loaded.meta.release.plannedCommands
|
|
2157
|
+
: release?.commands || options.releasePlan?.commands || [],
|
|
1844
2158
|
lastVerifiedAt: verifiedAt,
|
|
1845
2159
|
},
|
|
1846
2160
|
verification: {
|
|
@@ -49,7 +49,7 @@ openxiangda sdd context --changed --change <change> --json
|
|
|
49
49
|
openxiangda sdd propose <change>
|
|
50
50
|
# record the user's approval, then:
|
|
51
51
|
openxiangda sdd approve <change>
|
|
52
|
-
openxiangda sdd verify <change> --changed --stage implementation
|
|
52
|
+
openxiangda sdd verify <change> --changed --stage implementation --profile <name>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
For an eligible L1 edit, create the exact approved record directly; the user's concrete request is the approval, so do not add a redundant proposal/approval loop:
|
|
@@ -60,6 +60,10 @@ openxiangda sdd quick <change> --kind copy --pages <page> --files <file> --summa
|
|
|
60
60
|
|
|
61
61
|
Function quick changes require the complete `--risk-json` assessment; Automation, Workflow, schema, permissions, auth/public access, migrations, destructive and cross-resource changes cannot use quick mode. Before release run `--stage prepublish`; use `postpublish` only after promotion and `archive` only when all evidence exists.
|
|
62
62
|
|
|
63
|
+
Keep development lightweight: `sdd verify --stage implementation` checks scope/contracts but does not block on unfinished task/evidence prose. Plain `sdd verify` means prepublish, not all future stages. Do not manufacture postpublish/archive evidence during coding; the strict gates belong at stage/finalize.
|
|
64
|
+
|
|
65
|
+
`sdd verify/sync` preserves non-empty, reviewed `release.json.commands` and `change.json.release.plannedCommands` independently; it must not regenerate either into broader or directly activating commands. The prepublish gate also validates the actual argv, so a safe recorded plan cannot authorize a direct Backend/Form/Runtime activation. Generated React SPA plans use exact Form bundles, one mixed Backend `--stage-only` command, Runtime `--no-activate`, and Root `app-finalize`, with explicit `--profile` and `--change` on every publish command. Each staged command automatically merges its canonical child entry into `.openxiangda/releases/<change>/staged-resources.json`.
|
|
66
|
+
|
|
63
67
|
## Safe release recipes
|
|
64
68
|
|
|
65
69
|
Classic workspace, exact page/form:
|
|
@@ -77,10 +81,11 @@ React SPA resources/runtime:
|
|
|
77
81
|
openxiangda resource validate <type> --only <codes> --profile <name>
|
|
78
82
|
openxiangda resource plan <type> --only <codes> --profile <name> --json
|
|
79
83
|
openxiangda runtime deploy --change <change> --profile <name> --no-activate --json
|
|
80
|
-
openxiangda sdd verify <change> --changed --stage prepublish
|
|
84
|
+
openxiangda sdd verify <change> --changed --stage prepublish --profile <name>
|
|
81
85
|
openxiangda release begin --change <change> --profile <name>
|
|
82
|
-
openxiangda resource publish
|
|
83
|
-
openxiangda
|
|
86
|
+
openxiangda resource publish function,automation --only <qualified-codes> --stage-only --change <change> --profile <name>
|
|
87
|
+
openxiangda resource publish form-setting --only <form-codes> --change <change> --profile <name>
|
|
88
|
+
openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>
|
|
84
89
|
# Merge/fast-forward the frozen release commit into the remote default branch.
|
|
85
90
|
openxiangda release integration-status --profile <name>
|
|
86
91
|
openxiangda release end --profile <name>
|
|
@@ -96,7 +101,7 @@ After promotion, immediately merge or fast-forward the exact frozen publish comm
|
|
|
96
101
|
|
|
97
102
|
For a whole-app release, keep changed Runtime/Page/Backend/Form children staged and run `openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>`. The JSON contains only changed immutable child entries; the CLI overlays Runtime/Page/Backend by singleton kind and FormRelease by `formUuid` onto one authoritative read-only capture, preserving every unmodified active child. It then performs `prepare -> verify -> activateStagedChildren=true`, switching child heads and the App head atomically (`atomic_staged_children_v1`, or `atomic_staged_children_v2` when a Backend Release v2 child is present). Never refresh or retry after a conflict. The flow requires the owned stored lease/change baseline and carries the same client session and Git lineage on every write. Omitting the overlay is compatibility-only retrospective aggregation of already-active children.
|
|
98
103
|
|
|
99
|
-
When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default.
|
|
104
|
+
When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Backend Release v2 accepts per-resource create, source-only update, and manifest replacement update in one immutable child and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. `--stage-only` is fail-closed: every selected mutation must enter that child, and a missing/incompatible Backend Release API never falls back to direct writes. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged unless exact `--replace-manifest --reason "..."` authority was provided.
|
|
100
105
|
|
|
101
106
|
An App Function may declare metadata-only top-level `secretRefs: [{ name, required }]` only with `function_v2` + `trusted_node_v2`; source resolves values with `await ctx.secrets.get(name)` and uses `ctx.utils.http` for controlled public HTTPS. Create/rotate values through hidden TTY or `openxiangda secret ... --value-stdin --change <change> --profile <name>`. Never put values in arguments, files, manifests, state, plans, logs, errors, or chat. Secret bindings require `backend_release_v2` and whole-app `atomic_staged_children_v2`; a missing capability is fail-closed and never uses the legacy source PATCH. For whole-app activation use exact-scope `resource publish <type> --only <code> --stage-only`, then pass the returned verified `stagedResource` to `release app-finalize`; an active Backend Release is never labeled staged.
|
|
102
107
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Code Page Publish Flow
|
|
2
2
|
|
|
3
|
-
Preferred
|
|
3
|
+
Preferred React SPA release entry:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
openxiangda workspace
|
|
6
|
+
openxiangda workspace plan --change <change> --changed --profile <name>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Do not run `lowcode-workspace publish-all`, `pnpm publish:all`, or legacy
|
|
@@ -11,7 +11,8 @@ project scripts such as `scripts/openxiangda-publish.mjs` directly for live
|
|
|
11
11
|
publishing. Those commands are workspace internals. They may skip OpenXiangda's
|
|
12
12
|
normal-user token/profile injection and fall back to old `APP_KEY` /
|
|
13
13
|
`APP_SECRET` behavior, or publish with a buildId that the platform menu does not
|
|
14
|
-
activate. Use
|
|
14
|
+
activate. Use the staged Form/Backend/Runtime commands emitted by the workspace
|
|
15
|
+
plan, then one Root App finalize.
|
|
15
16
|
|
|
16
17
|
For day-to-day AI edits, prefer a targeted publish:
|
|
17
18
|
|
|
@@ -23,15 +24,12 @@ openxiangda workspace check --changed
|
|
|
23
24
|
# Preview what git-touched forms/pages would publish.
|
|
24
25
|
openxiangda workspace publish --profile <name> --changed --dry-run
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
-
openxiangda
|
|
27
|
+
# Stage one exact Form bundle; React page changes are staged in Runtime.
|
|
28
|
+
openxiangda resource publish form-setting --only customer --change <change> --profile <name>
|
|
29
|
+
openxiangda runtime deploy --no-activate --change <change> --profile <name>
|
|
28
30
|
|
|
29
|
-
#
|
|
30
|
-
openxiangda
|
|
31
|
-
openxiangda workspace publish --profile <name> --form customer
|
|
32
|
-
|
|
33
|
-
# Publish several explicit modules.
|
|
34
|
-
openxiangda workspace publish --profile <name> --only pages/dashboard,forms/customer
|
|
31
|
+
# Atomically activate every staged child and the App head.
|
|
32
|
+
openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
Do not use full workspace publish as a reflex. Full publish is only appropriate
|
|
@@ -43,13 +41,14 @@ Targeted publish skips `src/resources` by default. Add `--resources` if resource
|
|
|
43
41
|
manifests changed too, or run `openxiangda resource plan|publish` separately.
|
|
44
42
|
Use `--skip-resources` when you explicitly want forms/pages only.
|
|
45
43
|
|
|
46
|
-
React SPA workspaces use a fixed release order:
|
|
44
|
+
React SPA workspaces use a fixed staged release order:
|
|
47
45
|
|
|
48
46
|
```bash
|
|
49
|
-
openxiangda workspace plan --changed --json
|
|
50
|
-
openxiangda
|
|
51
|
-
openxiangda resource publish --profile <name>
|
|
52
|
-
openxiangda runtime deploy --profile <name>
|
|
47
|
+
openxiangda workspace plan --change <change> --changed --profile <name> --json
|
|
48
|
+
openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
|
|
49
|
+
openxiangda resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change> --profile <name>
|
|
50
|
+
openxiangda runtime deploy --no-activate --change <change> --profile <name>
|
|
51
|
+
openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
|
|
53
52
|
```
|
|
54
53
|
|
|
55
54
|
If `workspace check --changed` reports risky AI fallback code, missing schema,
|
|
@@ -26,6 +26,8 @@ Run `openxiangda update check --json` once per substantial task, on a suspected
|
|
|
26
26
|
|
|
27
27
|
Every write/release command must include `--profile <name>`. In a shared checkout, create an isolated Git worktree and named feature branch before editing. `sdd propose|quick|context` claims `.openxiangda/worktree-owner.json` for the current `CODEX_THREAD_ID`; a foreign owner is a hard stop. `.openxiangda/state.json` maps profile-local IDs; it is not a collaboration lock. The task is not complete until the exact frozen publish SHA is merged without squash/rebase and pushed to the authoritative default branch.
|
|
28
28
|
|
|
29
|
+
SDD is permissive during coding and strict at release: `sdd verify --stage implementation` does not require unfinished checklist/evidence prose, and an omitted stage means prepublish rather than postpublish/archive. Exact command, child scope, and mainline gates apply only when staging or activating writes.
|
|
30
|
+
|
|
29
31
|
First-class direct configuration writes (form/page/menu/role/permission/workflow/connector/notification/auth/route/public-access/scope/data-view/storage/settings) also require `--change <id>` or a complete stored session created by `openxiangda release begin --change <id>`. Missing context fails locally with `PUBLISH_CONTEXT_REQUIRED` before any HTTP write. Dry-run, schema planning, validation, query/invoke/export/submit/execution/upload, and other data-plane actions do not acquire a lease. Direct Workflow publish/unpublish/delete additionally requires the frozen `--expected-revision`; never fetch the latest revision merely to replay an older payload.
|
|
30
32
|
|
|
31
33
|
## Exact planning
|
|
@@ -55,11 +57,13 @@ openxiangda resource publish function --only function_a,function_b --profile <na
|
|
|
55
57
|
|
|
56
58
|
Selectors apply before manifest parsing, source dependency analysis, and JS_CODE typecheck/build. A scoped Function/Automation plan must touch only the selected targets plus their transitive/shared/ambient dependencies; an unscoped plan intentionally retains full-workspace behavior. Resource commands use the canonical scoped builder packaged with the installed CLI for standard workspaces, so old checked-in builders still receive the optimization; refresh/bootstrap the workspace script only for equivalent manual `pnpm build-js-code` behavior. Nonstandard custom builders remain an explicit compatibility fallback.
|
|
57
59
|
|
|
58
|
-
Source-triggered Function/Automation targets use
|
|
60
|
+
Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
|
|
59
61
|
|
|
60
62
|
Functions with a top-level `secretRefs` field use `backend_release_v2`, including an explicit empty list that removes bindings. This path never falls back to source PATCH. It requires the per-app Secret capability probe to grant `app_function_secrets_v1`, `trusted_node_v2`, `backend_release_v2`, and `atomic_staged_children_v2`; otherwise plan/publish fails closed.
|
|
61
63
|
|
|
62
|
-
For whole-app atomic activation, publish the exact Function/Automation scope with `resource publish
|
|
64
|
+
For whole-app atomic activation, publish the exact mixed Function/Automation scope with `resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change>`. This stops after Backend Release verify and returns `stagedResource` plus every handled selector; the CLI automatically merges it with other changed staged children in `.openxiangda/releases/<change>/staged-resources.json` for `release app-finalize --staged-resources-json`. A Backend Release that was already activated returns `activeResource` and must not be presented as staged.
|
|
65
|
+
|
|
66
|
+
Standard workspaces use the CLI-bundled scoped JS_CODE builder directly, including isolated Git-base checks; do not invoke `pnpm` merely to rebuild a selected Function/Automation. New snapshots carry `source_lineage_v1.sourceHash`, which is derived from authored source/dependencies rather than generated bundle bytes, so a builder upgrade does not falsely report source divergence.
|
|
63
67
|
|
|
64
68
|
Use `openxiangda release backend-head|backend-list|backend-detail|backend-diff` to inspect immutable history. `backend-rollback <releaseId> --change <change> --reason "..."` prepares, verifies, and activates a new release from the historical snapshot; it never mutates or directly reactivates the old row. `backend-abort` stops prepared/verified work and `backend-retry` retries only recorded post-commit side effects. Do not fetch newer revisions to replay an older payload.
|
|
65
69
|
|
|
@@ -80,16 +84,16 @@ Do not call `lowcode-workspace publish-all`, `pnpm publish:all`, `pnpm publish:o
|
|
|
80
84
|
|
|
81
85
|
## React SPA
|
|
82
86
|
|
|
83
|
-
Forms
|
|
87
|
+
Forms, Backend resources, and frontend Runtime are staged children of one Root App release:
|
|
84
88
|
|
|
85
89
|
```bash
|
|
86
|
-
openxiangda
|
|
90
|
+
openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
|
|
87
91
|
openxiangda resource plan <type> --only <codes> --profile <name>
|
|
88
92
|
openxiangda runtime deploy --change <change> --profile <name> --no-activate --json
|
|
89
|
-
openxiangda sdd verify <change> --changed --stage prepublish
|
|
93
|
+
openxiangda sdd verify <change> --changed --stage prepublish --profile <name>
|
|
90
94
|
openxiangda release begin --change <change> --profile <name>
|
|
91
|
-
openxiangda resource publish
|
|
92
|
-
openxiangda
|
|
95
|
+
openxiangda resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change> --profile <name>
|
|
96
|
+
openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
|
|
93
97
|
# Merge/fast-forward the frozen release SHA to the remote default branch.
|
|
94
98
|
openxiangda release integration-status --profile <name>
|
|
95
99
|
openxiangda release end --profile <name>
|
|
@@ -23,9 +23,10 @@ openxiangda form list --profile <name>
|
|
|
23
23
|
# src/forms/<formCode>/schema.ts — fields, options, rules, top-level FormEffect[]
|
|
24
24
|
# src/forms/<formCode>/page.tsx — presentation only
|
|
25
25
|
|
|
26
|
-
# 3.
|
|
27
|
-
openxiangda
|
|
28
|
-
openxiangda
|
|
26
|
+
# 3. normal release: stage the exact Form bundle, then Root App finalize
|
|
27
|
+
openxiangda resource plan form-setting --only <formCode> --profile <name>
|
|
28
|
+
openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
|
|
29
|
+
openxiangda release app-finalize --staged-resources-json .openxiangda/releases/<change>/staged-resources.json --change <change> --profile <name>
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
## DO / DO NOT
|
|
@@ -71,19 +72,19 @@ src/forms/<formCode>/
|
|
|
71
72
|
└── page.tsx
|
|
72
73
|
```
|
|
73
74
|
|
|
74
|
-
Then
|
|
75
|
+
Then stage the exact Form bundle:
|
|
75
76
|
|
|
76
77
|
```bash
|
|
77
|
-
openxiangda
|
|
78
|
+
openxiangda resource publish form-setting --only <formCodes> --change <change> --profile <name>
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
For a single form edit,
|
|
81
|
+
For a single form edit, still keep the child staged until Root App finalize:
|
|
81
82
|
|
|
82
83
|
```bash
|
|
83
|
-
openxiangda
|
|
84
|
+
openxiangda resource publish form-setting --only <formCode> --change <change> --profile <name>
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
The
|
|
87
|
+
The Form resource bundle carries schema, settings, indexes, data-management, runtime-write, and public-access configuration under one CAS parent. It returns a canonical staged FormRelease and the CLI records it in the change-scoped staged-resources file. `workspace publish --form` is limited to an intentional first-time bootstrap or isolated repair outside a governed multi-resource release; it is not the normal release path.
|
|
87
88
|
|
|
88
89
|
For Phase 6 React SPA workspaces, `app-workspace.config.ts` should declare
|
|
89
90
|
`runtimeMode: "react-spa"`. In that mode, `workspace publish --form <code>` is
|
|
@@ -109,7 +110,7 @@ Do not use `openxiangda form create` as the normal way to generate a user-facing
|
|
|
109
110
|
## Form Release / CAS Safety
|
|
110
111
|
|
|
111
112
|
- Treat all Form configuration as one immutable release stream. Freeze `revision`, `etag`, and `activeFormReleaseHead` from one snapshot before writing.
|
|
112
|
-
- Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one Form `bundle` mutation. Resource Form bundles are stage-only by default
|
|
113
|
+
- Publish declared schema, packages, settings, indexes, data-management, runtime-write, and public-access configuration through one `resource publish form-setting --only <codes> --change <change>` Form `bundle` mutation. Resource Form bundles are stage-only by default and return immutable `contentHash` plus a canonical `stagedResource` (stable identity is `formUuid`); the CLI merges each entry into `.openxiangda/releases/<change>/staged-resources.json`. Root `app-finalize` atomically switches all child heads plus the App head. Use direct `--activate` only for an intentional form-only repair outside a governed multi-resource release; never use `workspace publish --form` inside an atomic React SPA release.
|
|
113
114
|
- Every mutation must carry `If-Match`, `expectedRevision`, `expectedParent`, and a stable artifact ID. Creation starts at revision `0`.
|
|
114
115
|
- A revision/head conflict is not retryable. Stop with zero later writes, re-plan, and explicitly merge the competing change; never silently adopt the newer live head.
|
|
115
116
|
- Use `openxiangda form release-head|release-list|release-detail|release-diff` for inspection and `release-activate|release-rollback|release-abort` for controlled recovery. See `docs/openxiangda-form-releases.md` in the OpenXiangda tool repository.
|