openxiangda 1.0.172 → 1.0.173
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +695 -102
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +46 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +178 -41
- package/openxiangda-skills/SKILL.md +4 -6
- package/openxiangda-skills/references/openxiangda-api.md +6 -6
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/permissions-settings.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +4 -6
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -4
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/templates/openxiangda-react-spa/AGENTS.md +5 -11
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -8
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
package/lib/runtime-lineage.js
CHANGED
|
@@ -2,6 +2,7 @@ const path = require('path');
|
|
|
2
2
|
const { spawnSync } = require('child_process');
|
|
3
3
|
|
|
4
4
|
const { readGitSourceBase } = require('./change-baseline');
|
|
5
|
+
const { identitiesIntersect } = require('./git-remote-identity');
|
|
5
6
|
|
|
6
7
|
const MIN_ROLLBACK_REASON_LENGTH = 8;
|
|
7
8
|
const PURE_RUNTIME_OUTPUT_PREFIXES = Object.freeze([
|
|
@@ -58,7 +59,13 @@ function prepareRuntimeDeployLineage(options = {}) {
|
|
|
58
59
|
|
|
59
60
|
const legacyBootstrap = Boolean(parentReleaseId && !parentSourceRevision);
|
|
60
61
|
if (parentSourceRevision) {
|
|
61
|
-
const sameRepository =
|
|
62
|
+
const sameRepository = identitiesIntersect(
|
|
63
|
+
[
|
|
64
|
+
parentSourceRevision.repo,
|
|
65
|
+
...(parentSourceRevision.repoAliases || []),
|
|
66
|
+
],
|
|
67
|
+
[sourceRevision.repo, ...(sourceRevision.repoAliases || [])]
|
|
68
|
+
);
|
|
62
69
|
const descendsFromActive =
|
|
63
70
|
sameRepository &&
|
|
64
71
|
isGitAncestor(cwd, parentSourceRevision.baseCommit, sourceRevision.baseCommit);
|
|
@@ -73,7 +80,13 @@ function prepareRuntimeDeployLineage(options = {}) {
|
|
|
73
80
|
);
|
|
74
81
|
}
|
|
75
82
|
return stripUndefinedValues({
|
|
76
|
-
sourceRevision
|
|
83
|
+
sourceRevision: sameRepository
|
|
84
|
+
? {
|
|
85
|
+
...sourceRevision,
|
|
86
|
+
repo: parentSourceRevision.repo,
|
|
87
|
+
repositoryId: parentSourceRevision.repo,
|
|
88
|
+
}
|
|
89
|
+
: sourceRevision,
|
|
77
90
|
parentReleaseId,
|
|
78
91
|
parentSourceRevision,
|
|
79
92
|
lineageBypassReason: rollback.enabled ? rollback.reason : undefined,
|
|
@@ -246,7 +259,14 @@ function normalizeRuntimeSourceRevision(value, options = {}) {
|
|
|
246
259
|
'线上 Runtime source revision 缺少有效的 repo/baseCommit/treeHash'
|
|
247
260
|
);
|
|
248
261
|
}
|
|
249
|
-
|
|
262
|
+
const repoAliases = Array.from(
|
|
263
|
+
new Set(
|
|
264
|
+
[repo, ...(Array.isArray(input.repoAliases) ? input.repoAliases : [])]
|
|
265
|
+
.map(item => String(item || '').trim().toLowerCase())
|
|
266
|
+
.filter(item => /^sha256:[0-9a-f]{64}$/.test(item))
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
return { repo, repoAliases, baseCommit, treeHash };
|
|
250
270
|
}
|
|
251
271
|
|
|
252
272
|
function normalizeAppSnapshot(value) {
|
package/lib/sdd.js
CHANGED
|
@@ -3,6 +3,11 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { readGitSourceBase } = require('./change-baseline');
|
|
5
5
|
const { assertReleaseSourceIntegrated } = require('./release-mainline');
|
|
6
|
+
const {
|
|
7
|
+
buildWorkspaceReleaseCommands,
|
|
8
|
+
normalizeReleaseTargets,
|
|
9
|
+
releasePlanHash,
|
|
10
|
+
} = require('./release-plan');
|
|
6
11
|
|
|
7
12
|
const DEFAULT_SDD_DIR = 'openspec';
|
|
8
13
|
const SDD_CONFIG_FILE = 'config.yaml';
|
|
@@ -868,6 +873,19 @@ function createMainlineSddBundle(options = {}) {
|
|
|
868
873
|
if (affected.functions.length > 0 || affected.automations.length > 0) {
|
|
869
874
|
affected.backend = true;
|
|
870
875
|
}
|
|
876
|
+
const runtimeMode = /runtimeMode\s*:\s*['"]react-spa['"]/.test(
|
|
877
|
+
options.configText || ''
|
|
878
|
+
)
|
|
879
|
+
? 'react-spa'
|
|
880
|
+
: 'legacy';
|
|
881
|
+
const normalizedTargets = normalizeReleaseTargets(affected, runtimeMode);
|
|
882
|
+
const profile = String(options.profile || '<profile>').trim() || '<profile>';
|
|
883
|
+
const commands = buildWorkspaceReleaseCommands(
|
|
884
|
+
normalizedTargets.logicalTargets,
|
|
885
|
+
runtimeMode,
|
|
886
|
+
profile,
|
|
887
|
+
changeId
|
|
888
|
+
);
|
|
871
889
|
const createdAt = nowIso();
|
|
872
890
|
const meta = {
|
|
873
891
|
...createChangeMetadata(changeId, {
|
|
@@ -913,19 +931,18 @@ function createMainlineSddBundle(options = {}) {
|
|
|
913
931
|
changeId,
|
|
914
932
|
mainlinePolicy: 'publish-from-main-v1',
|
|
915
933
|
bundledChanges: sourceIds,
|
|
916
|
-
targets:
|
|
917
|
-
|
|
918
|
-
pages: affected.pages,
|
|
919
|
-
functions: affected.functions,
|
|
920
|
-
automations: affected.automations,
|
|
921
|
-
workflows: affected.workflows,
|
|
922
|
-
jsCodeNodes: affected.jsCodeNodes,
|
|
923
|
-
resources: affected.resources,
|
|
924
|
-
runtime: affected.runtime,
|
|
925
|
-
other: [],
|
|
926
|
-
},
|
|
934
|
+
targets: normalizedTargets.activationTargets,
|
|
935
|
+
logicalTargets: normalizedTargets.logicalTargets,
|
|
927
936
|
changedFiles: affected.files,
|
|
928
|
-
commands
|
|
937
|
+
commands,
|
|
938
|
+
entryCommand:
|
|
939
|
+
`openxiangda release publish --change ${changeId} --profile ${profile}`,
|
|
940
|
+
planHash: releasePlanHash({
|
|
941
|
+
changeId,
|
|
942
|
+
runtimeMode,
|
|
943
|
+
targets: normalizedTargets.activationTargets,
|
|
944
|
+
commands,
|
|
945
|
+
}),
|
|
929
946
|
verifiedAt: null,
|
|
930
947
|
};
|
|
931
948
|
const changeDir = path.join(governance.rootDir, 'changes', changeId);
|
|
@@ -1847,14 +1864,24 @@ function buildUpdatedReleaseManifest(loaded, releasePlan, verificationStage = 'd
|
|
|
1847
1864
|
const canonicalCommands = Array.isArray(releasePlan?.commands)
|
|
1848
1865
|
? releasePlan.commands.filter(command => String(command || '').trim())
|
|
1849
1866
|
: [];
|
|
1867
|
+
const actualExecution = releasePlan?.actualCommands === true;
|
|
1850
1868
|
return {
|
|
1851
1869
|
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1852
1870
|
changeId: loaded.changeId,
|
|
1853
1871
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
1854
|
-
targets:
|
|
1855
|
-
|
|
1872
|
+
targets:
|
|
1873
|
+
actualExecution
|
|
1874
|
+
? loaded.release?.targets || {}
|
|
1875
|
+
: releasePlan?.activationTargets ||
|
|
1876
|
+
releasePlan?.targets ||
|
|
1877
|
+
loaded.release?.targets ||
|
|
1878
|
+
{},
|
|
1879
|
+
changedFiles:
|
|
1880
|
+
actualExecution
|
|
1881
|
+
? loaded.release?.changedFiles || []
|
|
1882
|
+
: releasePlan?.changedFiles || loaded.release?.changedFiles || [],
|
|
1856
1883
|
commands:
|
|
1857
|
-
canonicalCommands.length > 0
|
|
1884
|
+
!actualExecution && canonicalCommands.length > 0
|
|
1858
1885
|
? canonicalCommands
|
|
1859
1886
|
: reviewedCommands,
|
|
1860
1887
|
verifiedAt,
|
|
@@ -1872,6 +1899,37 @@ function updateReleaseManifest(loaded, releasePlan, verificationStage = 'default
|
|
|
1872
1899
|
return release;
|
|
1873
1900
|
}
|
|
1874
1901
|
|
|
1902
|
+
function writeReleaseAttestation(cwd, changeId, name, value) {
|
|
1903
|
+
const root = path.resolve(cwd || process.cwd());
|
|
1904
|
+
const dir = path.join(
|
|
1905
|
+
root,
|
|
1906
|
+
'.openxiangda',
|
|
1907
|
+
'releases',
|
|
1908
|
+
normalizeChangeId(changeId)
|
|
1909
|
+
);
|
|
1910
|
+
ensureDir(dir);
|
|
1911
|
+
const file = path.join(dir, name);
|
|
1912
|
+
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1913
|
+
fs.writeFileSync(tempFile, `${JSON.stringify(value, null, 2)}\n`, {
|
|
1914
|
+
mode: 0o600,
|
|
1915
|
+
});
|
|
1916
|
+
fs.renameSync(tempFile, file);
|
|
1917
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function readReleaseAttestation(cwd, changeId, name) {
|
|
1921
|
+
const file = path.join(
|
|
1922
|
+
path.resolve(cwd || process.cwd()),
|
|
1923
|
+
'.openxiangda',
|
|
1924
|
+
'releases',
|
|
1925
|
+
normalizeChangeId(changeId),
|
|
1926
|
+
name
|
|
1927
|
+
);
|
|
1928
|
+
if (!fs.existsSync(file)) return null;
|
|
1929
|
+
const value = readJsonFile(file, null);
|
|
1930
|
+
return value && !value.__invalid ? value : null;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1875
1933
|
function recordSddReleaseSourceRevision(options = {}) {
|
|
1876
1934
|
const loaded = loadSddChange(options);
|
|
1877
1935
|
const input = options.releaseSourceRevision;
|
|
@@ -1885,6 +1943,16 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1885
1943
|
branch: String(input.branch || '').trim() || null,
|
|
1886
1944
|
remoteName: String(input.remoteName || '').trim() || null,
|
|
1887
1945
|
remoteUrlHash: String(input.remoteUrlHash || '').trim().toLowerCase() || null,
|
|
1946
|
+
remoteUrlHashAliases: Array.isArray(input.remoteUrlHashAliases)
|
|
1947
|
+
? input.remoteUrlHashAliases
|
|
1948
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
1949
|
+
.filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
|
|
1950
|
+
: [],
|
|
1951
|
+
repoAliases: Array.isArray(input.repoAliases)
|
|
1952
|
+
? input.repoAliases
|
|
1953
|
+
.map(value => String(value || '').trim().toLowerCase())
|
|
1954
|
+
.filter(value => /^sha256:[0-9a-f]{64}$/.test(value))
|
|
1955
|
+
: [],
|
|
1888
1956
|
mainBranch: String(input.mainBranch || '').trim() || null,
|
|
1889
1957
|
mainTipCommit: String(input.mainTipCommit || '').trim().toLowerCase() || null,
|
|
1890
1958
|
capturedAt: input.capturedAt || nowIso(),
|
|
@@ -1901,7 +1969,13 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1901
1969
|
'releaseSourceRevision 缺少有效的 repo/baseCommit/treeHash/remoteName/remoteUrlHash/mainBranch'
|
|
1902
1970
|
);
|
|
1903
1971
|
}
|
|
1904
|
-
const existing =
|
|
1972
|
+
const existing =
|
|
1973
|
+
readReleaseAttestation(
|
|
1974
|
+
options.cwd || process.cwd(),
|
|
1975
|
+
loaded.changeId,
|
|
1976
|
+
'source-revision.json'
|
|
1977
|
+
)?.releaseSourceRevision ||
|
|
1978
|
+
loaded.release?.releaseSourceRevision;
|
|
1905
1979
|
if (
|
|
1906
1980
|
existing?.baseCommit &&
|
|
1907
1981
|
(existing.baseCommit !== releaseSourceRevision.baseCommit ||
|
|
@@ -1917,20 +1991,29 @@ function recordSddReleaseSourceRevision(options = {}) {
|
|
|
1917
1991
|
error.code = 'RELEASE_PUBLISH_REVISION_CHANGED';
|
|
1918
1992
|
throw error;
|
|
1919
1993
|
}
|
|
1920
|
-
|
|
1994
|
+
writeReleaseAttestation(
|
|
1995
|
+
options.cwd || process.cwd(),
|
|
1996
|
+
loaded.changeId,
|
|
1997
|
+
'source-revision.json',
|
|
1998
|
+
{
|
|
1999
|
+
schemaVersion: SDD_SCHEMA_VERSION,
|
|
2000
|
+
changeId: loaded.changeId,
|
|
2001
|
+
mainlinePolicy: 'publish-from-main-v1',
|
|
2002
|
+
releaseSourceRevision,
|
|
2003
|
+
}
|
|
2004
|
+
);
|
|
2005
|
+
return {
|
|
1921
2006
|
...(loaded.release && !loaded.release.__invalid ? loaded.release : {}),
|
|
1922
|
-
schemaVersion: SDD_SCHEMA_VERSION,
|
|
1923
|
-
changeId: loaded.changeId,
|
|
1924
|
-
mainlinePolicy: 'publish-from-main-v1',
|
|
1925
2007
|
releaseSourceRevision,
|
|
1926
2008
|
};
|
|
1927
|
-
if (JSON.stringify(release) !== JSON.stringify(loaded.release)) {
|
|
1928
|
-
writeJsonFile(loaded.releaseFile, release);
|
|
1929
|
-
}
|
|
1930
|
-
return release;
|
|
1931
2009
|
}
|
|
1932
2010
|
|
|
1933
|
-
function validateReleaseManifest(
|
|
2011
|
+
function validateReleaseManifest(
|
|
2012
|
+
release,
|
|
2013
|
+
releasePlan,
|
|
2014
|
+
legacyMode = false,
|
|
2015
|
+
options = {}
|
|
2016
|
+
) {
|
|
1934
2017
|
const errors = [];
|
|
1935
2018
|
const warnings = [];
|
|
1936
2019
|
if (!release || release.__invalid) {
|
|
@@ -1942,9 +2025,14 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
|
|
|
1942
2025
|
else errors.push(issue);
|
|
1943
2026
|
return { errors, warnings };
|
|
1944
2027
|
}
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
2028
|
+
const plannedTargets =
|
|
2029
|
+
releasePlan?.activationTargets || releasePlan?.targets || null;
|
|
2030
|
+
const targets = plannedTargets || release.targets || {};
|
|
2031
|
+
if (plannedTargets) {
|
|
2032
|
+
const releaseTargetCoverage = affectedCoversTargets(
|
|
2033
|
+
release.targets || {},
|
|
2034
|
+
plannedTargets
|
|
2035
|
+
);
|
|
1948
2036
|
if (!releaseTargetCoverage.covered) {
|
|
1949
2037
|
errors.push({
|
|
1950
2038
|
name: 'sdd-release-targets-uncovered',
|
|
@@ -1955,10 +2043,18 @@ function validateReleaseManifest(release, releasePlan, legacyMode = false) {
|
|
|
1955
2043
|
if (hasHighRiskTargets(targets) && !Array.isArray(release.commands)) {
|
|
1956
2044
|
errors.push({ name: 'sdd-release-commands-missing', message: 'release.json commands 必须是数组' });
|
|
1957
2045
|
}
|
|
1958
|
-
if (
|
|
2046
|
+
if (
|
|
2047
|
+
!options.actualExecution &&
|
|
2048
|
+
releasePlan?.commands?.length &&
|
|
2049
|
+
(!Array.isArray(release.commands) || release.commands.length === 0)
|
|
2050
|
+
) {
|
|
1959
2051
|
errors.push({ name: 'sdd-release-commands-empty', message: 'release.json 未记录发布命令' });
|
|
1960
2052
|
}
|
|
1961
|
-
if (
|
|
2053
|
+
if (
|
|
2054
|
+
!legacyMode &&
|
|
2055
|
+
Array.isArray(release.commands) &&
|
|
2056
|
+
release.commands.length > 0
|
|
2057
|
+
) {
|
|
1962
2058
|
errors.push(
|
|
1963
2059
|
...validateAtomicReleaseCommands(
|
|
1964
2060
|
release.commands,
|
|
@@ -2288,12 +2384,25 @@ function verifySddChange(options = {}) {
|
|
|
2288
2384
|
if (strictDocumentation) errors.push(issue);
|
|
2289
2385
|
else warnings.push(issue);
|
|
2290
2386
|
}
|
|
2387
|
+
const actualExecution = options.releasePlan?.actualCommands === true;
|
|
2291
2388
|
const release =
|
|
2292
|
-
options.releasePlan &&
|
|
2293
|
-
|
|
2389
|
+
options.releasePlan &&
|
|
2390
|
+
loaded.release &&
|
|
2391
|
+
!loaded.release.__invalid &&
|
|
2392
|
+
!actualExecution
|
|
2393
|
+
? buildUpdatedReleaseManifest(
|
|
2394
|
+
loaded,
|
|
2395
|
+
options.releasePlan,
|
|
2396
|
+
verificationStage
|
|
2397
|
+
)
|
|
2294
2398
|
: loaded.release;
|
|
2295
2399
|
if (stagePolicy.requireRelease) {
|
|
2296
|
-
const releaseValidation = validateReleaseManifest(
|
|
2400
|
+
const releaseValidation = validateReleaseManifest(
|
|
2401
|
+
release,
|
|
2402
|
+
options.releasePlan,
|
|
2403
|
+
legacyMode,
|
|
2404
|
+
{ actualExecution }
|
|
2405
|
+
);
|
|
2297
2406
|
errors.push(...releaseValidation.errors);
|
|
2298
2407
|
warnings.push(...releaseValidation.warnings);
|
|
2299
2408
|
const reviewedPlannedCommands = loaded.meta?.release?.plannedCommands;
|
|
@@ -2347,11 +2456,28 @@ function verifySddChange(options = {}) {
|
|
|
2347
2456
|
updatedAt: verifiedAt,
|
|
2348
2457
|
};
|
|
2349
2458
|
let metadataWritten = false;
|
|
2459
|
+
let attestationFile = null;
|
|
2350
2460
|
if (options.writeMetadata && passed) {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2461
|
+
attestationFile = writeReleaseAttestation(
|
|
2462
|
+
options.cwd || process.cwd(),
|
|
2463
|
+
loaded.changeId,
|
|
2464
|
+
`verification-${verificationStage}.json`,
|
|
2465
|
+
{
|
|
2466
|
+
schemaVersion: SDD_SCHEMA_VERSION,
|
|
2467
|
+
changeId: loaded.changeId,
|
|
2468
|
+
stage: verificationStage,
|
|
2469
|
+
passed,
|
|
2470
|
+
verifiedAt,
|
|
2471
|
+
planHash: releasePlanHash({
|
|
2472
|
+
targets,
|
|
2473
|
+
commands: release?.commands || [],
|
|
2474
|
+
changedFiles:
|
|
2475
|
+
options.releasePlan?.changedFiles ||
|
|
2476
|
+
loaded.release?.changedFiles ||
|
|
2477
|
+
[],
|
|
2478
|
+
}),
|
|
2479
|
+
}
|
|
2480
|
+
);
|
|
2355
2481
|
metadataWritten = true;
|
|
2356
2482
|
}
|
|
2357
2483
|
return {
|
|
@@ -2365,6 +2491,8 @@ function verifySddChange(options = {}) {
|
|
|
2365
2491
|
blockingErrors: errors,
|
|
2366
2492
|
passed,
|
|
2367
2493
|
metadataWritten,
|
|
2494
|
+
trackedMetadataWritten: false,
|
|
2495
|
+
attestationFile,
|
|
2368
2496
|
targets,
|
|
2369
2497
|
highRiskFiles,
|
|
2370
2498
|
stage: verificationStage,
|
|
@@ -2559,6 +2687,15 @@ function syncSddChange(options = {}) {
|
|
|
2559
2687
|
|
|
2560
2688
|
function archiveSddChange(options = {}) {
|
|
2561
2689
|
const releaseLoaded = loadSddChange(options);
|
|
2690
|
+
const sourceRevisionAttestation = readReleaseAttestation(
|
|
2691
|
+
options.cwd || process.cwd(),
|
|
2692
|
+
releaseLoaded.changeId,
|
|
2693
|
+
'source-revision.json'
|
|
2694
|
+
);
|
|
2695
|
+
const releaseSourceRevision =
|
|
2696
|
+
sourceRevisionAttestation?.releaseSourceRevision ||
|
|
2697
|
+
releaseLoaded.release?.releaseSourceRevision ||
|
|
2698
|
+
null;
|
|
2562
2699
|
const coverageTargets = normalizeCoverageResources(releaseLoaded.coverage, {});
|
|
2563
2700
|
const releaseTargets = mergeAffectedScopes(
|
|
2564
2701
|
options.targets,
|
|
@@ -2595,20 +2732,20 @@ function archiveSddChange(options = {}) {
|
|
|
2595
2732
|
classifyHighRiskFiles(releaseTargets.files).length > 0 ||
|
|
2596
2733
|
metadataDeclaresHighRisk ||
|
|
2597
2734
|
Boolean(releaseLoaded.release?.mainlinePolicy) ||
|
|
2598
|
-
Boolean(
|
|
2735
|
+
Boolean(releaseSourceRevision);
|
|
2599
2736
|
if (requiresMainlineClosure) {
|
|
2600
2737
|
if (
|
|
2601
2738
|
!releaseLoaded.release ||
|
|
2602
2739
|
releaseLoaded.release.__invalid ||
|
|
2603
|
-
!
|
|
2740
|
+
!releaseSourceRevision
|
|
2604
2741
|
) {
|
|
2605
2742
|
const error = new Error(
|
|
2606
|
-
'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change
|
|
2743
|
+
'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED: 高风险 change 归档必须保留冻结发布提交的本地 attestation;--force 不能绕过主分支回合证据'
|
|
2607
2744
|
);
|
|
2608
2745
|
error.code = 'SDD_RELEASE_SOURCE_LINEAGE_REQUIRED';
|
|
2609
2746
|
throw error;
|
|
2610
2747
|
}
|
|
2611
|
-
assertReleaseSourceIntegrated(
|
|
2748
|
+
assertReleaseSourceIntegrated(releaseSourceRevision, {
|
|
2612
2749
|
cwd: options.cwd || process.cwd(),
|
|
2613
2750
|
});
|
|
2614
2751
|
}
|
|
@@ -82,16 +82,14 @@ React SPA resources/runtime:
|
|
|
82
82
|
# In the clean main checkout:
|
|
83
83
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
84
84
|
# Commit and push the generated bundle on main, then:
|
|
85
|
-
openxiangda
|
|
86
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
87
|
-
# Execute the canonical exact commands printed by workspace plan/verify.
|
|
88
|
-
openxiangda release integration-status --profile <name>
|
|
89
|
-
openxiangda release end --profile <name>
|
|
85
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
90
86
|
```
|
|
91
87
|
|
|
92
88
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
|
|
93
89
|
|
|
94
|
-
`release
|
|
90
|
+
`release publish` is the default promotion entrypoint. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. `release begin` and child commands remain recovery/diagnostic primitives.
|
|
91
|
+
|
|
92
|
+
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences are aliases of the same repository; genuinely different remotes still fail closed.
|
|
95
93
|
|
|
96
94
|
Because promotion begins from the already-pushed authoritative mainline, `release integration-status` should pass immediately after activation. Run it, then `release end`; no post-release branch merge is required.
|
|
97
95
|
|
|
@@ -442,13 +442,13 @@ Requires app admin permission. Upserts a binding with `templateCode` or `templat
|
|
|
442
442
|
|
|
443
443
|
## Organization
|
|
444
444
|
|
|
445
|
-
Organization APIs are app-scoped
|
|
445
|
+
Organization APIs are app-scoped. List/detail operations accept `app:organization:read` or `app:organization:manage`; create/update/password operations require `app:organization:manage`. Platform admins pass through the same permission service. Runtime service principals are not accepted by themselves: trusted runtime calls must carry a real audit actor, and permission is checked against that user.
|
|
446
446
|
|
|
447
447
|
Use these endpoints for OpenXiangda app-managed platform departments and accounts. Do not use legacy `/user` or `/department` write endpoints for new app organization management.
|
|
448
448
|
|
|
449
449
|
### GET `/apps/:appType/organization/capabilities`
|
|
450
450
|
|
|
451
|
-
Requires Bearer token. Returns
|
|
451
|
+
Requires Bearer token. Returns `canRead`, `canManage`, `readPermissionCode`, `managePermissionCode`, the compatibility `permissionCode`, and supported operations.
|
|
452
452
|
|
|
453
453
|
CLI:
|
|
454
454
|
|
|
@@ -458,11 +458,11 @@ openxiangda organization capabilities --json
|
|
|
458
458
|
|
|
459
459
|
### GET `/apps/:appType/organization/departments`
|
|
460
460
|
|
|
461
|
-
Requires `app:organization:manage`. Lists platform departments in the current tenant.
|
|
461
|
+
Requires `app:organization:read` or `app:organization:manage`. Lists platform departments in the current tenant.
|
|
462
462
|
|
|
463
463
|
### GET `/apps/:appType/organization/departments/:departmentId`
|
|
464
464
|
|
|
465
|
-
Requires `app:organization:manage`. Returns one platform department.
|
|
465
|
+
Requires `app:organization:read` or `app:organization:manage`. Returns one platform department.
|
|
466
466
|
|
|
467
467
|
### POST `/apps/:appType/organization/departments`
|
|
468
468
|
|
|
@@ -492,11 +492,11 @@ Requires `app:organization:manage`. Updates department metadata, visibility/memb
|
|
|
492
492
|
|
|
493
493
|
### GET `/apps/:appType/organization/accounts`
|
|
494
494
|
|
|
495
|
-
Requires `app:organization:manage`. Lists tenant platform accounts. Supports `ids`, `departmentIds`, `keyword`, `name`, `username`, `phone`, `email`, `jobNumber`, `page`, and `pageSize`.
|
|
495
|
+
Requires `app:organization:read` or `app:organization:manage`. Lists tenant platform accounts. Supports `ids`, `departmentIds`, `keyword`, `name`, `username`, `phone`, `email`, `jobNumber`, `page`, and `pageSize`.
|
|
496
496
|
|
|
497
497
|
### GET `/apps/:appType/organization/accounts/:userId`
|
|
498
498
|
|
|
499
|
-
Requires `app:organization:manage`. Returns one platform account. Password, login failure counters, and lock fields are removed from the response.
|
|
499
|
+
Requires `app:organization:read` or `app:organization:manage`. Returns one platform account. Password, login failure counters, and lock fields are removed from the response.
|
|
500
500
|
|
|
501
501
|
### POST `/apps/:appType/organization/accounts`
|
|
502
502
|
|
|
@@ -17,7 +17,7 @@ Guidelines:
|
|
|
17
17
|
- Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
|
|
18
18
|
- Use `sdk.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
|
|
19
19
|
- Use `AttachmentPreviewList`, `ImagePreviewGrid`, or `useFilePreview` from `openxiangda/runtime/react` for in-page attachment previews in a custom React SPA page. They use the current PageSdk context and enforce the platform capability and ticket contracts. Use `sdk.createFileAccessTicket(bucketName, objectName, fileName, "preview", { appType })` only when the page needs a shareable or new-window preview link. `appType` defaults to the current page context. Open `response.result.previewPageUrl`; do not use `previewUrl` or `/service/file/preview-by-ticket/:ticket` as the page entry.
|
|
20
|
-
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for
|
|
20
|
+
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for intentional organization pages. Read-only list/detail pages need `app:organization:read` or `app:organization:manage`; writes and password operations need `app:organization:manage`. Do not call legacy `/user` or `/department` endpoints from pages.
|
|
21
21
|
- Use `sdk.workCenter.listItems({ boxType })` and `sdk.workCenter.getStats()` for current-user work center lists and counts. `boxType` is one of `todo`, `done`, `cc`, or `initiated`. This is the end-user task/handled/cc/initiated surface; do not build todo pages by reading workflow operation logs, automation logs, or raw process-task tables.
|
|
22
22
|
- `sdk.form.create` returns identifiers and generated serial number values only. Treat `formInstId` / `formInstanceId` as the required success contract; `serialNumber` / `serialNumbers` are present only when the form has `SerialNumberField`. Do not expect the save response to contain the full row, formula results, or other server-generated field values. Call `sdk.form.getDetail` explicitly after create when those values are needed.
|
|
23
23
|
- Missing `formInstId` / `formInstanceId` is a real error in strict mode. Do not create fake IDs, mock rows, or fallback display data to hide it.
|
|
@@ -33,9 +33,10 @@ in `src/resources/roles/<code>.json`; otherwise the user may have the business
|
|
|
33
33
|
role but still fail when calling role-setting APIs.
|
|
34
34
|
|
|
35
35
|
For app-managed platform accounts or departments, use `sdk.organization.*` or
|
|
36
|
-
`ctx.organization.*`. The real current operator
|
|
37
|
-
`app:organization:manage
|
|
38
|
-
|
|
36
|
+
`ctx.organization.*`. The real current operator needs `app:organization:read`
|
|
37
|
+
or `app:organization:manage` for list/detail queries and
|
|
38
|
+
`app:organization:manage` for writes/password operations; do not bypass
|
|
39
|
+
OpenXiangda with legacy user or department APIs.
|
|
39
40
|
|
|
40
41
|
## Role Codes
|
|
41
42
|
|
|
@@ -89,14 +89,12 @@ Forms, Backend resources, and frontend Runtime are staged children of one Root A
|
|
|
89
89
|
```bash
|
|
90
90
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
91
91
|
# Commit and push the bundle on main/master.
|
|
92
|
-
openxiangda
|
|
93
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
94
|
-
# Run the canonical exact staged commands emitted by the release plan.
|
|
95
|
-
openxiangda release integration-status --profile <name>
|
|
96
|
-
openxiangda release end --profile <name>
|
|
92
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
97
93
|
```
|
|
98
94
|
|
|
99
|
-
`
|
|
95
|
+
`release publish` is the normal whole-app entrypoint: it verifies SDD without mutating reviewed files, waits for the promotion lease, freezes one App capture, stages the exact Form/Backend/Runtime children, resumes from a private execution journal, finalizes once, and releases the lease. Individual release commands are recovery/diagnostic primitives.
|
|
96
|
+
|
|
97
|
+
`runtime deploy --no-activate` uploads an immutable preview release from a clean committed mainline `HEAD`. It reads a narrow Runtime head instead of the full app snapshot. Before acquiring a lease, the CLI confirms `package.json#scripts.build` and existing dependencies; it uses `npm run build` to execute the declared script so pnpm worktree symlinks do not trigger a reinstall. Immutable Git-base artifact hashes are cached under the Git common directory. `release begin` requires local main/master and the live remote default tip to be identical, so `integration-status` is already satisfied after activation and `release end` does not wait for a later merge.
|
|
100
98
|
|
|
101
99
|
`resource plan`, `resource publish`, and `runtime deploy` report coarse phases on stderr and emit a heartbeat after 15 seconds without completion. JSON stdout stays machine-readable and includes `timings`; use it to identify whether time is spent in plan, preflight, build, upload, write, or activate instead of rerunning an opaque command.
|
|
102
100
|
|
|
@@ -22,7 +22,7 @@ description: Manage OpenXiangda app **roles** (角色), **page permission groups
|
|
|
22
22
|
- ✅ For dynamic multi-role apps, build a role maintenance form + automation/JS_CODE sync, not hardcoded role logic in page code.
|
|
23
23
|
- ✅ For permission scope keys, derive hidden scalars (`collegeScopeKey`, `classScopeKey`, `ownerDeptScopeKey`, `ownerUserScopeKey`, `roleCode`) via `valueSync` from visible select/person/department fields.
|
|
24
24
|
- ✅ Use page permission groups for entry visibility, form permission groups for real data isolation with condition-based data permissions.
|
|
25
|
-
- ✅ For app-managed platform accounts/departments, use `sdk.organization.*` / `ctx.organization
|
|
25
|
+
- ✅ For app-managed platform accounts/departments, use `sdk.organization.*` / `ctx.organization.*`. Read-only list/detail operations require `app:organization:read` or `app:organization:manage`; writes and password operations require `app:organization:manage`.
|
|
26
26
|
- ✅ For delegated administrators such as 校区管理员, put role-setting API permissions in the role manifest with `apiPermissionCodes`; use `app:role:manage` for app role creation/member assignment/API-permission assignment, and add page/form/organization management permission codes only when needed.
|
|
27
27
|
- ✅ If a delegated administrator creates another role that can also manage accounts, roles, or permission groups, publish that new role with its own `apiPermissionCodes`.
|
|
28
28
|
- ❌ Expose raw ID text fields to users for permission scopes.
|
|
@@ -85,9 +85,10 @@ For dynamic multi-role apps, do not hardcode role behavior only in page code. Cr
|
|
|
85
85
|
For managed platform account governance, use organization/account forms and sync
|
|
86
86
|
functions as a closed loop: organization units, system accounts, role
|
|
87
87
|
assignments, roles resources, page groups, form groups, sync App Functions, and
|
|
88
|
-
backend role/scope checks. Platform account
|
|
89
|
-
`sdk.organization` or `ctx.organization`;
|
|
90
|
-
`app:organization:manage
|
|
88
|
+
backend role/scope checks. Platform account access must go through
|
|
89
|
+
`sdk.organization` or `ctx.organization`; give read-only operators
|
|
90
|
+
`app:organization:read` and require `app:organization:manage` for writes and
|
|
91
|
+
password operations.
|
|
91
92
|
|
|
92
93
|
## Page Permission Groups
|
|
93
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.173",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"test:publish-lease-state": "node scripts/publish-lease-state-smoke.mjs",
|
|
87
87
|
"test:publish-lease-heartbeat": "node scripts/publish-lease-heartbeat-smoke.mjs",
|
|
88
88
|
"test:release-telemetry": "node scripts/release-telemetry-smoke.mjs",
|
|
89
|
+
"test:release-plan": "node scripts/release-plan-smoke.mjs",
|
|
89
90
|
"test:release-mainline": "node scripts/release-mainline-smoke.mjs",
|
|
90
91
|
"test:publish-lease-cli": "node scripts/publish-lease-cli-smoke.mjs",
|
|
91
92
|
"test:publish-context": "node scripts/publish-context-smoke.mjs",
|
|
@@ -264,6 +264,9 @@ interface AppFunctionNotificationApi {
|
|
|
264
264
|
interface OrganizationCapabilities {
|
|
265
265
|
appType: string;
|
|
266
266
|
permissionCode: "app:organization:manage" | string;
|
|
267
|
+
readPermissionCode: "app:organization:read" | string;
|
|
268
|
+
managePermissionCode: "app:organization:manage" | string;
|
|
269
|
+
canRead: boolean;
|
|
267
270
|
canManage: boolean;
|
|
268
271
|
actor?: Record<string, unknown>;
|
|
269
272
|
runtimeServicePrincipalRequiresAuditActor: boolean;
|
|
@@ -264,6 +264,9 @@ interface AppFunctionNotificationApi {
|
|
|
264
264
|
interface OrganizationCapabilities {
|
|
265
265
|
appType: string;
|
|
266
266
|
permissionCode: "app:organization:manage" | string;
|
|
267
|
+
readPermissionCode: "app:organization:read" | string;
|
|
268
|
+
managePermissionCode: "app:organization:manage" | string;
|
|
269
|
+
canRead: boolean;
|
|
267
270
|
canManage: boolean;
|
|
268
271
|
actor?: Record<string, unknown>;
|
|
269
272
|
runtimeServicePrincipalRequiresAuditActor: boolean;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 一个开发任务只使用一个显式 change,并从 `openxiangda sdd context --change <change> --changed --json` 开始;结构化 approval、coverage 资源和文件范围是硬约束,任务/证据/规格文案默认只告警,不阻断开发或发布。只有明确配置 `strictDocumentation: true` 才把文案完成度恢复为门禁。
|
|
10
10
|
- 多会话开发使用独立 Git worktree/branch,但 feature worktree 不发布。把已批准提交合并并 push 到权威默认主分支后,用 `sdd bundle <release-change> --changes ...` 聚合范围;只从与远端 tip 完全一致的 clean `main`/`master` 一次发布。
|
|
11
11
|
- 账号、角色、权限、数据范围、组织账号、RBAC、查询参数授权需求必须先运行 `openxiangda design gates --topic permissions --json`,选择 `managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,输出权限矩阵后再实现。
|
|
12
|
-
-
|
|
12
|
+
- 应用角色只读查询组织账号时声明 `app:organization:read`;创建、修改账号/部门或重置密码时声明 `app:organization:manage`。创建角色、分配成员或维护权限组也必须在角色资源的 `apiPermissionCodes` 声明对应的 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`。
|
|
13
13
|
- 默认用户界面保持克制:左侧应用导航、顶部账号信息、首页内容区域。
|
|
14
14
|
- 不在默认可见页面展示 SDK、Runtime、Cookie、Proxy、Playwright、AI 验证、调试上下文、构建号等开发语言。
|
|
15
15
|
- 使用 React Router 管理路由,路由定义在 `src/app/router.tsx`。
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- 菜单和首页文案优先改 `src/app/navigation.ts`、`src/app/starter-content.ts` 与 `src/pages/admin/AdminDashboardPage.tsx`。
|
|
18
18
|
- 页面、表单、字段、数据范围、流程动作、文件、连接器权限以后端接口为准;前端只做展示保护和清晰状态页。
|
|
19
19
|
- 查询参数只能作为上下文、筛选或 ticket 输入,不能作为敏感数据授权依据;敏感读写必须由 public-access grants、平台角色、页面/表单权限组或 App Function 后端校验保护。
|
|
20
|
-
-
|
|
20
|
+
- 应用查询平台账号/部门时必须走 `sdk.organization` / `ctx.organization`,当前操作者至少具备 `app:organization:read`;写入和密码操作必须具备 `app:organization:manage`。
|
|
21
21
|
- 只给用户分配“管理员”业务角色不等于授权其设置角色;缺少 `app:role:manage` 时角色创建、成员分配和角色接口权限授予会被后端拒绝。
|
|
22
22
|
- 本地开发通过 Vite `/service` 代理远端平台,保持 HttpOnly Cookie 同域访问。
|
|
23
23
|
- 新公开访问页使用 `/view/:appType/public/*`、`src/resources/routes/`、`src/resources/public-access/` 和 `PublicAccessGate`。不要使用旧 `?publicAccess=guest`。
|
|
@@ -40,10 +40,7 @@ openxiangda sdd context --change <change> --changed --json
|
|
|
40
40
|
openxiangda sdd verify <change> --changed --stage implementation
|
|
41
41
|
openxiangda resource plan <type> --only <code1,code2> --profile <name>
|
|
42
42
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
43
|
-
openxiangda
|
|
44
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
45
|
-
openxiangda release integration-status --profile <name>
|
|
46
|
-
openxiangda release end --profile <name>
|
|
43
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
47
44
|
openxiangda commands --json
|
|
48
45
|
```
|
|
49
46
|
|
|
@@ -56,11 +53,8 @@ openxiangda commands --json
|
|
|
56
53
|
```bash
|
|
57
54
|
# 先合并所有 approved task commits 到 main/master 并 push
|
|
58
55
|
openxiangda sdd bundle <release-change> --changes <change-a,change-b>
|
|
59
|
-
# commit/push bundle
|
|
60
|
-
openxiangda
|
|
61
|
-
openxiangda release begin --change <release-change> --profile <name>
|
|
62
|
-
openxiangda release integration-status --profile <name>
|
|
63
|
-
openxiangda release end --profile <name>
|
|
56
|
+
# commit/push bundle 后由一个可恢复命令完成 verify、排队、暂存和原子激活
|
|
57
|
+
openxiangda release publish --change <release-change> --profile <name>
|
|
64
58
|
```
|
|
65
59
|
|
|
66
60
|
`pnpm build-js-code` 会检查并打包 `src/js-code-nodes/<code>/index.ts`、`src/automations/<code>/index.ts`、`src/functions/<code>/index.ts`,供 JS_CODE V2、代码自动化和 App Function 资源发布使用。批量目标使用 `pnpm build-js-code --scripts functions:a,functions:b,automations:c`,也兼容重复的 `--script a --script b --source functions`。正式 `resource plan/publish` 由 CLI 内置 scoped builder 一次批量构建选中入口及其传递/shared/ambient 依赖,不再为每个资源启动工作区 `pnpm`。缓存写入 `.openxiangda/build-cache.cli-v4.json`,稳定 `source_lineage_v1` 只按 authored source/dependencies 比较,不因构建器升级制造假冲突;需要强制重建时追加 `--force`。
|