openxiangda 1.0.164 → 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 +9 -7
- package/lib/change-baseline.js +74 -6
- package/lib/cli.js +2508 -167
- package/lib/sdd.js +320 -6
- package/lib/utils.js +76 -2
- package/openxiangda-skills/SKILL.md +13 -6
- package/openxiangda-skills/references/pages/publish-flow.md +15 -16
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +24 -2
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +14 -6
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +10 -9
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -0
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/index.cjs +164 -48
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +164 -48
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +164 -48
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +79 -1
- package/packages/sdk/dist/runtime/react.d.ts +79 -1
- package/packages/sdk/dist/runtime/react.mjs +164 -48
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/openxiangda-react-spa/AGENTS.md +12 -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/.cursor/rules/openxiangda-resources.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +5 -3
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +20 -2
package/lib/cli.js
CHANGED
|
@@ -114,12 +114,14 @@ const {
|
|
|
114
114
|
verifySddChange,
|
|
115
115
|
} = require('./sdd');
|
|
116
116
|
const {
|
|
117
|
+
clearSensitiveText,
|
|
117
118
|
fail,
|
|
118
119
|
formatFetchError,
|
|
119
120
|
maskText,
|
|
120
121
|
openBrowser,
|
|
121
122
|
parseArgs,
|
|
122
123
|
print,
|
|
124
|
+
registerSensitiveText,
|
|
123
125
|
sleep,
|
|
124
126
|
warn,
|
|
125
127
|
writeJson,
|
|
@@ -130,6 +132,12 @@ const NPM_PACKAGE_NAME = 'openxiangda';
|
|
|
130
132
|
const OFFICIAL_NPM_REGISTRY = 'https://registry.npmjs.org';
|
|
131
133
|
const RUNTIME_UPLOAD_DEFAULT_TIMEOUT_MS = 120000;
|
|
132
134
|
const RUNTIME_UPLOAD_CONCURRENCY = 3;
|
|
135
|
+
const APP_FUNCTION_SECRET_CAPABILITIES = Object.freeze({
|
|
136
|
+
contractVersion: 'app_function_secrets_v1',
|
|
137
|
+
runtimeContractVersion: 'trusted_node_v2',
|
|
138
|
+
backendRelease: 'backend_release_v2',
|
|
139
|
+
atomicActivation: 'atomic_staged_children_v2',
|
|
140
|
+
});
|
|
133
141
|
let readOnlyHttpGuardDepth = 0;
|
|
134
142
|
let readOnlyHttpGuardLabel = null;
|
|
135
143
|
let activePublishRequestContext = null;
|
|
@@ -179,6 +187,7 @@ async function mainImpl(argv) {
|
|
|
179
187
|
if (command === 'public-access') return publicAccess(rest);
|
|
180
188
|
if (command === 'auth-config') return authConfig(rest);
|
|
181
189
|
if (command === 'function') return appFunction(rest);
|
|
190
|
+
if (command === 'secret') return appSecret(rest);
|
|
182
191
|
if (command === 'connector') return connector(rest);
|
|
183
192
|
if (command === 'notification') return notification(rest);
|
|
184
193
|
if (command === 'organization') return organization(rest);
|
|
@@ -256,7 +265,9 @@ Usage:
|
|
|
256
265
|
openxiangda route list|get|create|update|upsert|delete [--json-file file] [--dry-run] [--write-manifest]
|
|
257
266
|
openxiangda public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check [--json-file file]
|
|
258
267
|
openxiangda auth-config list|get|create|update|upsert|delete|methods [--json-file file]
|
|
259
|
-
openxiangda function list|get|create|update|upsert|delete|invoke [--json-file file]
|
|
268
|
+
openxiangda function list|get|create|update|upsert|delete|invoke|test [--json-file file]
|
|
269
|
+
openxiangda secret list|metadata|references|audit|unused [name] --profile <name>
|
|
270
|
+
openxiangda secret create|update|rotate|delete <name> --profile <name> --change <id> [--expected-revision n] [--value-stdin] [--dry-run]
|
|
260
271
|
openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [--json-file file]
|
|
261
272
|
openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send
|
|
262
273
|
openxiangda organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password
|
|
@@ -304,10 +315,12 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
304
315
|
'--resources',
|
|
305
316
|
'--skip-resources',
|
|
306
317
|
'--show-secret',
|
|
318
|
+
'--stage-only',
|
|
307
319
|
'--strict',
|
|
308
320
|
'--sdd-bypass',
|
|
309
321
|
'--summary',
|
|
310
322
|
'--unpublish',
|
|
323
|
+
'--value-stdin',
|
|
311
324
|
'--write-manifest',
|
|
312
325
|
'--yes',
|
|
313
326
|
'-h',
|
|
@@ -370,6 +383,12 @@ async function version(args) {
|
|
|
370
383
|
const result = {
|
|
371
384
|
name: NPM_PACKAGE_NAME,
|
|
372
385
|
version: CURRENT_VERSION,
|
|
386
|
+
contracts: {
|
|
387
|
+
appFunctionSecrets: APP_FUNCTION_SECRET_CAPABILITIES.contractVersion,
|
|
388
|
+
trustedNode: APP_FUNCTION_SECRET_CAPABILITIES.runtimeContractVersion,
|
|
389
|
+
backendRelease: APP_FUNCTION_SECRET_CAPABILITIES.backendRelease,
|
|
390
|
+
atomicActivation: APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation,
|
|
391
|
+
},
|
|
373
392
|
updateCheckCommand: 'openxiangda update check --json',
|
|
374
393
|
};
|
|
375
394
|
if (flags.json) return writeJson(result);
|
|
@@ -971,6 +990,7 @@ async function release(args) {
|
|
|
971
990
|
);
|
|
972
991
|
}
|
|
973
992
|
const pendingBaseline = getStoredChangeBaseline(target);
|
|
993
|
+
const newReleaseLifecycle = !stored && !pendingBaseline;
|
|
974
994
|
const clientSessionId =
|
|
975
995
|
existing?.clientSessionId ||
|
|
976
996
|
(pendingBaseline?.changeId === changeId &&
|
|
@@ -999,12 +1019,16 @@ async function release(args) {
|
|
|
999
1019
|
throw error;
|
|
1000
1020
|
}
|
|
1001
1021
|
const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
|
|
1022
|
+
const stagedResourcesFile = newReleaseLifecycle
|
|
1023
|
+
? initializeStagedAppReleaseResources(target, changeId)
|
|
1024
|
+
: null;
|
|
1002
1025
|
const result = {
|
|
1003
1026
|
...lease,
|
|
1004
1027
|
baselineId: finalizedBaseline?.baselineId,
|
|
1005
1028
|
baselineHeadDigest: finalizedBaseline?.headDigest,
|
|
1006
1029
|
sourceBase: finalizedBaseline?.sourceBase,
|
|
1007
1030
|
releaseSourceRevision: finalizedBaseline?.releaseSourceRevision,
|
|
1031
|
+
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
1008
1032
|
};
|
|
1009
1033
|
if (flags.json) return writeJson(result);
|
|
1010
1034
|
print(
|
|
@@ -1543,6 +1567,285 @@ function readStagedAppReleaseResources(flags = {}) {
|
|
|
1543
1567
|
);
|
|
1544
1568
|
}
|
|
1545
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
|
+
|
|
1834
|
+
function stagedAppReleaseProtocolVersion(stagedResources) {
|
|
1835
|
+
const hasBackendReleaseV2 = (stagedResources || []).some(
|
|
1836
|
+
resource =>
|
|
1837
|
+
resource.kind === 'BackendRelease' &&
|
|
1838
|
+
String(
|
|
1839
|
+
resource.metadata?.protocolVersion ||
|
|
1840
|
+
resource.revision?.protocolVersion ||
|
|
1841
|
+
''
|
|
1842
|
+
) === APP_FUNCTION_SECRET_CAPABILITIES.backendRelease
|
|
1843
|
+
);
|
|
1844
|
+
return hasBackendReleaseV2
|
|
1845
|
+
? APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation
|
|
1846
|
+
: null;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1546
1849
|
function overlayAppReleaseResources(capturedResources, stagedResources) {
|
|
1547
1850
|
if (!Array.isArray(capturedResources)) {
|
|
1548
1851
|
fail('APP_RELEASE_CAPTURE_INVALID: capture.resources 必须是数组');
|
|
@@ -1614,6 +1917,9 @@ async function prepareAppReleaseFromCapture(
|
|
|
1614
1917
|
{
|
|
1615
1918
|
method: 'POST',
|
|
1616
1919
|
body: appReleaseWriteBody(releaseContext, {
|
|
1920
|
+
...(flags.__appReleaseProtocolVersion
|
|
1921
|
+
? { protocolVersion: flags.__appReleaseProtocolVersion }
|
|
1922
|
+
: {}),
|
|
1617
1923
|
parentReleaseId: capture.parentReleaseId || null,
|
|
1618
1924
|
resources: clonePlainJson(capture.resources),
|
|
1619
1925
|
...(expectedManifestHash ? { expectedManifestHash } : {}),
|
|
@@ -1627,7 +1933,8 @@ async function verifyAppReleaseDetail(
|
|
|
1627
1933
|
target,
|
|
1628
1934
|
releaseContext,
|
|
1629
1935
|
detail,
|
|
1630
|
-
resources
|
|
1936
|
+
resources,
|
|
1937
|
+
options = {}
|
|
1631
1938
|
) {
|
|
1632
1939
|
if (!detail?.id || !/^[a-f0-9]{64}$/i.test(String(detail.manifestHash || ''))) {
|
|
1633
1940
|
fail('APP_RELEASE_MANIFEST_INVALID: App Release 缺少 id 或 manifestHash');
|
|
@@ -1642,6 +1949,9 @@ async function verifyAppReleaseDetail(
|
|
|
1642
1949
|
{
|
|
1643
1950
|
method: 'POST',
|
|
1644
1951
|
body: appReleaseWriteBody(releaseContext, {
|
|
1952
|
+
...(options.protocolVersion
|
|
1953
|
+
? { protocolVersion: options.protocolVersion }
|
|
1954
|
+
: {}),
|
|
1645
1955
|
manifestHash: detail.manifestHash,
|
|
1646
1956
|
resources: clonePlainJson(resources),
|
|
1647
1957
|
}),
|
|
@@ -1667,9 +1977,12 @@ async function activateAppReleaseDetail(
|
|
|
1667
1977
|
method: 'POST',
|
|
1668
1978
|
body: appReleaseWriteBody(
|
|
1669
1979
|
releaseContext,
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1980
|
+
stripUndefinedValues({
|
|
1981
|
+
...(options.activateStagedChildren === true
|
|
1982
|
+
? { activateStagedChildren: true }
|
|
1983
|
+
: {}),
|
|
1984
|
+
protocolVersion: options.protocolVersion,
|
|
1985
|
+
})
|
|
1673
1986
|
),
|
|
1674
1987
|
}
|
|
1675
1988
|
);
|
|
@@ -1723,6 +2036,15 @@ async function runAppReleaseCommand(
|
|
|
1723
2036
|
);
|
|
1724
2037
|
} else if (subcommand === 'app-prepare') {
|
|
1725
2038
|
const stagedResources = readStagedAppReleaseResources(flags);
|
|
2039
|
+
if (stagedResources) {
|
|
2040
|
+
assertStagedAppReleaseScope(target, flags, stagedResources);
|
|
2041
|
+
}
|
|
2042
|
+
const protocolVersion = stagedAppReleaseProtocolVersion(stagedResources);
|
|
2043
|
+
if (protocolVersion) {
|
|
2044
|
+
assertAppFunctionSecretCapabilities(
|
|
2045
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
1726
2048
|
const releaseContext = await ensureAppReleasePublishContext(
|
|
1727
2049
|
config,
|
|
1728
2050
|
target,
|
|
@@ -1739,34 +2061,78 @@ async function runAppReleaseCommand(
|
|
|
1739
2061
|
target,
|
|
1740
2062
|
releaseContext,
|
|
1741
2063
|
{ ...capture, resources },
|
|
1742
|
-
|
|
2064
|
+
{
|
|
2065
|
+
...flags,
|
|
2066
|
+
__appReleaseProtocolVersion: protocolVersion,
|
|
2067
|
+
}
|
|
1743
2068
|
);
|
|
1744
2069
|
} else if (subcommand === 'app-verify') {
|
|
1745
2070
|
if (!releaseId) {
|
|
1746
2071
|
fail('用法: openxiangda release app-verify <releaseId> --change <id>');
|
|
1747
2072
|
}
|
|
2073
|
+
const detail = await requestWithAuth(
|
|
2074
|
+
config,
|
|
2075
|
+
target.profileName,
|
|
2076
|
+
appReleaseApiPath(target, `/${encodeURIComponent(releaseId)}`)
|
|
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
|
+
}
|
|
2087
|
+
const protocolVersion = String(detail?.protocolVersion || '');
|
|
2088
|
+
if (protocolVersion === APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation) {
|
|
2089
|
+
assertAppFunctionSecretCapabilities(
|
|
2090
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
1748
2093
|
const releaseContext = await ensureAppReleasePublishContext(
|
|
1749
2094
|
config,
|
|
1750
2095
|
target,
|
|
1751
2096
|
flags,
|
|
1752
2097
|
'App Release verify'
|
|
1753
2098
|
);
|
|
1754
|
-
const detail = await requestWithAuth(
|
|
1755
|
-
config,
|
|
1756
|
-
target.profileName,
|
|
1757
|
-
appReleaseApiPath(target, `/${encodeURIComponent(releaseId)}`)
|
|
1758
|
-
);
|
|
1759
2099
|
data = await verifyAppReleaseDetail(
|
|
1760
2100
|
config,
|
|
1761
2101
|
target,
|
|
1762
2102
|
releaseContext,
|
|
1763
2103
|
detail,
|
|
1764
|
-
|
|
2104
|
+
detailResources,
|
|
2105
|
+
{ protocolVersion: protocolVersion || undefined }
|
|
1765
2106
|
);
|
|
1766
2107
|
} else if (subcommand === 'app-activate') {
|
|
1767
2108
|
if (!releaseId) {
|
|
1768
2109
|
fail('用法: openxiangda release app-activate <releaseId> --change <id>');
|
|
1769
2110
|
}
|
|
2111
|
+
const detail = await requestWithAuth(
|
|
2112
|
+
config,
|
|
2113
|
+
target.profileName,
|
|
2114
|
+
appReleaseApiPath(target, `/${encodeURIComponent(releaseId)}`)
|
|
2115
|
+
);
|
|
2116
|
+
const protocolVersion = String(detail?.protocolVersion || '');
|
|
2117
|
+
const atomicV2 =
|
|
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
|
+
}
|
|
2131
|
+
if (atomicV2) {
|
|
2132
|
+
assertAppFunctionSecretCapabilities(
|
|
2133
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
1770
2136
|
const releaseContext = await ensureAppReleasePublishContext(
|
|
1771
2137
|
config,
|
|
1772
2138
|
target,
|
|
@@ -1780,13 +2146,36 @@ async function runAppReleaseCommand(
|
|
|
1780
2146
|
releaseId,
|
|
1781
2147
|
{
|
|
1782
2148
|
activateStagedChildren:
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
'true',
|
|
2149
|
+
activatesStagedChildren,
|
|
2150
|
+
protocolVersion: protocolVersion || undefined,
|
|
1786
2151
|
}
|
|
1787
2152
|
);
|
|
1788
2153
|
} else if (subcommand === 'app-finalize') {
|
|
1789
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
|
+
}
|
|
2173
|
+
const protocolVersion = stagedAppReleaseProtocolVersion(stagedResources);
|
|
2174
|
+
if (protocolVersion) {
|
|
2175
|
+
assertAppFunctionSecretCapabilities(
|
|
2176
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
1790
2179
|
const releaseContext = await ensureAppReleasePublishContext(
|
|
1791
2180
|
config,
|
|
1792
2181
|
target,
|
|
@@ -1803,37 +2192,56 @@ async function runAppReleaseCommand(
|
|
|
1803
2192
|
sourceRepositoryId: releaseContext.sourceRepositoryId,
|
|
1804
2193
|
sourceRevision: releaseContext.sourceRevision,
|
|
1805
2194
|
resources: frozenResources,
|
|
2195
|
+
...(protocolVersion ? { protocolVersion } : {}),
|
|
1806
2196
|
});
|
|
1807
2197
|
const prepared = await prepareAppReleaseFromCapture(
|
|
1808
2198
|
config,
|
|
1809
2199
|
target,
|
|
1810
2200
|
releaseContext,
|
|
1811
2201
|
{ ...capture, resources: frozenResources },
|
|
1812
|
-
|
|
2202
|
+
{
|
|
2203
|
+
...flags,
|
|
2204
|
+
__appReleaseProtocolVersion: protocolVersion,
|
|
2205
|
+
}
|
|
1813
2206
|
);
|
|
1814
2207
|
const verified = await verifyAppReleaseDetail(
|
|
1815
2208
|
config,
|
|
1816
2209
|
target,
|
|
1817
2210
|
releaseContext,
|
|
1818
2211
|
prepared,
|
|
1819
|
-
frozenResources
|
|
2212
|
+
frozenResources,
|
|
2213
|
+
{ protocolVersion }
|
|
1820
2214
|
);
|
|
1821
2215
|
const activated = await activateAppReleaseDetail(
|
|
1822
2216
|
config,
|
|
1823
2217
|
target,
|
|
1824
2218
|
releaseContext,
|
|
1825
2219
|
verified?.id || prepared.id,
|
|
1826
|
-
{
|
|
2220
|
+
{
|
|
2221
|
+
activateStagedChildren: Boolean(stagedResources),
|
|
2222
|
+
protocolVersion,
|
|
2223
|
+
}
|
|
1827
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
|
+
}
|
|
1828
2235
|
data = {
|
|
1829
2236
|
activationModel: stagedResources
|
|
1830
|
-
? 'atomic_staged_children_v1'
|
|
2237
|
+
? protocolVersion || 'atomic_staged_children_v1'
|
|
1831
2238
|
: 'retrospective_active_children',
|
|
1832
2239
|
captureHash: capture.captureHash,
|
|
1833
2240
|
payloadHash,
|
|
1834
2241
|
prepared,
|
|
1835
2242
|
verified,
|
|
1836
2243
|
activated,
|
|
2244
|
+
...(stateReconcileWarning ? { stateReconcileWarning } : {}),
|
|
1837
2245
|
};
|
|
1838
2246
|
} else if (subcommand === 'app-rollback') {
|
|
1839
2247
|
if (!releaseId) {
|
|
@@ -3873,7 +4281,19 @@ async function workspace(args) {
|
|
|
3873
4281
|
if (publishOptions.dryRun) {
|
|
3874
4282
|
printWorkspaceReleasePlan(sddPlan);
|
|
3875
4283
|
} else {
|
|
3876
|
-
|
|
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
|
+
);
|
|
3877
4297
|
}
|
|
3878
4298
|
await requestWithAuth(
|
|
3879
4299
|
config,
|
|
@@ -6042,9 +6462,17 @@ async function appFunction(args) {
|
|
|
6042
6462
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
6043
6463
|
const { flags, positional } = parseArgs(rest);
|
|
6044
6464
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
6045
|
-
print(
|
|
6465
|
+
print([
|
|
6466
|
+
'用法: openxiangda function list|get|create|update|upsert|delete|invoke [functionCode] [--json-file file]',
|
|
6467
|
+
' openxiangda function test <functionCode> [--input-json JSON|file] [--secret-from-env logical=ENV] [--json]',
|
|
6468
|
+
'说明: function test 只把父进程环境变量中的值通过 stdin 交给隔离测试子进程;不会写入 workspace/cache/state。',
|
|
6469
|
+
].join('\n'));
|
|
6046
6470
|
return;
|
|
6047
6471
|
}
|
|
6472
|
+
if (subcommand === 'test') {
|
|
6473
|
+
return await testAppFunctionLocally(positional, flags);
|
|
6474
|
+
}
|
|
6475
|
+
|
|
6048
6476
|
const config = loadConfig();
|
|
6049
6477
|
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
6050
6478
|
|
|
@@ -6093,6 +6521,11 @@ async function appFunction(args) {
|
|
|
6093
6521
|
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/functions`,
|
|
6094
6522
|
manifestDir: path.join('src', 'resources', 'functions'),
|
|
6095
6523
|
prepareBody: async body => {
|
|
6524
|
+
if (functionManifestDeclaresSecretRefs(body)) {
|
|
6525
|
+
fail(
|
|
6526
|
+
'APP_FUNCTION_SECRET_STAGED_RELEASE_REQUIRED: 带 secretRefs 的 Function 必须使用 resource publish + Backend Release v2,禁止 direct CRUD 绕过原子绑定'
|
|
6527
|
+
);
|
|
6528
|
+
}
|
|
6096
6529
|
const definitionJson = await resolveManifestJson(
|
|
6097
6530
|
config,
|
|
6098
6531
|
target.profileName,
|
|
@@ -6100,6 +6533,11 @@ async function appFunction(args) {
|
|
|
6100
6533
|
'definitionJson',
|
|
6101
6534
|
'definitionFile'
|
|
6102
6535
|
);
|
|
6536
|
+
if (definitionJson?.secretRefs !== undefined) {
|
|
6537
|
+
fail(
|
|
6538
|
+
'APP_FUNCTION_SECRET_STAGED_RELEASE_REQUIRED: secretRefs 必须位于 Function manifest 顶层并通过 Backend Release v2 发布,禁止写入 definitionJson 或 direct CRUD'
|
|
6539
|
+
);
|
|
6540
|
+
}
|
|
6103
6541
|
applyResourceBindingsToRuntimeDefinition(target.bound, body, definitionJson);
|
|
6104
6542
|
const code = body.code || body.functionCode || body.resourceCode;
|
|
6105
6543
|
return stripUndefinedValues({
|
|
@@ -6120,67 +6558,1042 @@ async function appFunction(args) {
|
|
|
6120
6558
|
});
|
|
6121
6559
|
}
|
|
6122
6560
|
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
6127
|
-
print('用法: openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [connectorCode[.apiCode]] [--json-file file]');
|
|
6128
|
-
return;
|
|
6129
|
-
}
|
|
6130
|
-
const config = loadConfig();
|
|
6131
|
-
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
6132
|
-
|
|
6133
|
-
if (subcommand === 'invoke' || subcommand === 'download-test') {
|
|
6134
|
-
const targetName = positional[0] || flags.connector;
|
|
6135
|
-
if (!targetName) fail(`用法: openxiangda connector ${subcommand} <connectorCode.apiCode> [--body-json file|json]`);
|
|
6136
|
-
const parsed = parseConnectorApiName(targetName, flags);
|
|
6137
|
-
const body = {
|
|
6138
|
-
connector: parsed.connector,
|
|
6139
|
-
api: parsed.api,
|
|
6140
|
-
...readDirectJsonBody(flags, 'connector invoke', { optional: true }),
|
|
6141
|
-
...(flags['path-params-json'] ? { pathParams: readJsonArg(flags['path-params-json'], 'path-params-json') } : {}),
|
|
6142
|
-
...(flags['query-json'] ? { query: readJsonArg(flags['query-json'], 'query-json') } : {}),
|
|
6143
|
-
...(flags['headers-json'] ? { headers: readJsonArg(flags['headers-json'], 'headers-json') } : {}),
|
|
6144
|
-
...(flags['request-body-type'] ? { requestBodyType: flags['request-body-type'] } : {}),
|
|
6145
|
-
...(flags['response-type'] ? { responseType: flags['response-type'] } : {}),
|
|
6146
|
-
};
|
|
6147
|
-
if (subcommand === 'download-test') body.responseType = 'binary';
|
|
6148
|
-
return runDirectRequest(config, target, flags, {
|
|
6149
|
-
method: 'POST',
|
|
6150
|
-
path: `/${encodeURIComponent(target.appType)}/v1/connectors/actions/${subcommand === 'download-test' ? 'download' : 'invoke'}`,
|
|
6151
|
-
body,
|
|
6152
|
-
strictEnvelope: subcommand !== 'download-test',
|
|
6153
|
-
}, { controlPlaneMutation: false });
|
|
6154
|
-
}
|
|
6561
|
+
function appSecretApiPath(target, suffix = '') {
|
|
6562
|
+
return `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/secrets${suffix}`;
|
|
6563
|
+
}
|
|
6155
6564
|
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
positional,
|
|
6161
|
-
resourceType: 'connector',
|
|
6162
|
-
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`,
|
|
6163
|
-
manifestDir: path.join('src', 'resources', 'connectors'),
|
|
6164
|
-
createMethod: 'POST',
|
|
6165
|
-
createPath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
6166
|
-
createBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
6167
|
-
updateMethod: 'POST',
|
|
6168
|
-
updatePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
6169
|
-
updateBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
6170
|
-
normalizeBody: body => normalizeConnectorManifest(body),
|
|
6171
|
-
codeOf: body => body.code || body.methodName,
|
|
6172
|
-
responseData: data => Array.isArray(data?.data) ? data.data[0] : data,
|
|
6173
|
-
saveState: (code, data) => saveConnectorResource(target, code, data?.connector?.id || data?.id, {
|
|
6174
|
-
apis: Object.fromEntries(
|
|
6175
|
-
(data?.apis || []).map(api => [api.code || api.methodName, { apiId: api.id, name: api.name }])
|
|
6176
|
-
),
|
|
6177
|
-
}),
|
|
6178
|
-
});
|
|
6565
|
+
function manifestUsesAppFunctionSecrets(manifest) {
|
|
6566
|
+
return (manifest?.functions || []).some(
|
|
6567
|
+
item => functionManifestDeclaresSecretRefs(item)
|
|
6568
|
+
);
|
|
6179
6569
|
}
|
|
6180
6570
|
|
|
6181
|
-
|
|
6182
|
-
const
|
|
6183
|
-
const
|
|
6571
|
+
function collectCapabilityNames(value) {
|
|
6572
|
+
const names = new Set();
|
|
6573
|
+
const visit = item => {
|
|
6574
|
+
if (!item) return;
|
|
6575
|
+
if (typeof item === 'string') {
|
|
6576
|
+
names.add(item.trim().toLowerCase());
|
|
6577
|
+
return;
|
|
6578
|
+
}
|
|
6579
|
+
if (Array.isArray(item)) {
|
|
6580
|
+
item.forEach(visit);
|
|
6581
|
+
return;
|
|
6582
|
+
}
|
|
6583
|
+
if (typeof item === 'object') {
|
|
6584
|
+
for (const [key, enabled] of Object.entries(item)) {
|
|
6585
|
+
if (enabled === true) names.add(String(key).trim().toLowerCase());
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
};
|
|
6589
|
+
visit(value?.capabilities);
|
|
6590
|
+
visit(value?.features);
|
|
6591
|
+
visit(value?.contracts);
|
|
6592
|
+
return names;
|
|
6593
|
+
}
|
|
6594
|
+
|
|
6595
|
+
function appFunctionSecretCapabilityEnabled(capabilities, name) {
|
|
6596
|
+
const normalized = String(name || '').toLowerCase();
|
|
6597
|
+
const camel = normalized.replace(/_([a-z])/g, (_match, char) => char.toUpperCase());
|
|
6598
|
+
return (
|
|
6599
|
+
capabilities?.[normalized] === true ||
|
|
6600
|
+
capabilities?.[camel] === true ||
|
|
6601
|
+
capabilities?.capabilities?.[normalized] === true ||
|
|
6602
|
+
capabilities?.features?.[normalized] === true ||
|
|
6603
|
+
collectCapabilityNames(capabilities).has(normalized)
|
|
6604
|
+
);
|
|
6605
|
+
}
|
|
6606
|
+
|
|
6607
|
+
async function fetchAppFunctionSecretCapabilities(config, target) {
|
|
6608
|
+
return await requestWithAuth(
|
|
6609
|
+
config,
|
|
6610
|
+
target.profileName,
|
|
6611
|
+
appSecretApiPath(target, '/capabilities')
|
|
6612
|
+
);
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
function assertAppFunctionSecretCapabilities(capabilities) {
|
|
6616
|
+
const managementReady =
|
|
6617
|
+
capabilities?.enabled === true &&
|
|
6618
|
+
String(capabilities?.contractVersion || '') ===
|
|
6619
|
+
APP_FUNCTION_SECRET_CAPABILITIES.contractVersion &&
|
|
6620
|
+
String(capabilities?.runtimeContractVersion || '') ===
|
|
6621
|
+
APP_FUNCTION_SECRET_CAPABILITIES.runtimeContractVersion;
|
|
6622
|
+
const missing = [];
|
|
6623
|
+
if (!managementReady) missing.push('app_function_secrets_v1/trusted_node_v2');
|
|
6624
|
+
for (const feature of [
|
|
6625
|
+
'app_function_secrets_v1',
|
|
6626
|
+
'trusted_node_v2',
|
|
6627
|
+
APP_FUNCTION_SECRET_CAPABILITIES.backendRelease,
|
|
6628
|
+
APP_FUNCTION_SECRET_CAPABILITIES.atomicActivation,
|
|
6629
|
+
]) {
|
|
6630
|
+
if (!appFunctionSecretCapabilityEnabled(capabilities, feature)) {
|
|
6631
|
+
missing.push(feature);
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
if (missing.length > 0) {
|
|
6635
|
+
const error = new Error(
|
|
6636
|
+
`APP_FUNCTION_SECRET_CAPABILITY_REQUIRED: 平台尚未完整启用 ${unique(missing).join(', ')};禁止忽略 secretRefs 或降级发布`
|
|
6637
|
+
);
|
|
6638
|
+
error.code = 'APP_FUNCTION_SECRET_CAPABILITY_REQUIRED';
|
|
6639
|
+
error.missingCapabilities = unique(missing);
|
|
6640
|
+
throw error;
|
|
6641
|
+
}
|
|
6642
|
+
return capabilities;
|
|
6643
|
+
}
|
|
6644
|
+
|
|
6645
|
+
async function assertManifestAppFunctionSecretCapabilities(
|
|
6646
|
+
config,
|
|
6647
|
+
target,
|
|
6648
|
+
manifest
|
|
6649
|
+
) {
|
|
6650
|
+
if (!manifestUsesAppFunctionSecrets(manifest)) return null;
|
|
6651
|
+
try {
|
|
6652
|
+
return assertAppFunctionSecretCapabilities(
|
|
6653
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
6654
|
+
);
|
|
6655
|
+
} catch (error) {
|
|
6656
|
+
if (isHttpNotFound(error)) {
|
|
6657
|
+
const missing = new Error(
|
|
6658
|
+
'APP_FUNCTION_SECRET_CAPABILITY_REQUIRED: 当前平台没有 App Function Secret capability endpoint;禁止忽略 secretRefs 或按旧 Function 协议发布'
|
|
6659
|
+
);
|
|
6660
|
+
missing.code = 'APP_FUNCTION_SECRET_CAPABILITY_REQUIRED';
|
|
6661
|
+
throw missing;
|
|
6662
|
+
}
|
|
6663
|
+
throw error;
|
|
6664
|
+
}
|
|
6665
|
+
}
|
|
6666
|
+
|
|
6667
|
+
const SECRET_RESPONSE_FORBIDDEN_KEYS = new Set([
|
|
6668
|
+
'value',
|
|
6669
|
+
'secretvalue',
|
|
6670
|
+
'plaintext',
|
|
6671
|
+
'ciphertext',
|
|
6672
|
+
'cipher',
|
|
6673
|
+
'encryptedvalue',
|
|
6674
|
+
'encrypteddek',
|
|
6675
|
+
'wrappeddek',
|
|
6676
|
+
'valuehash',
|
|
6677
|
+
'cipherhash',
|
|
6678
|
+
'valuedigest',
|
|
6679
|
+
'cipherdigest',
|
|
6680
|
+
'valuelength',
|
|
6681
|
+
'cipherlength',
|
|
6682
|
+
'plaintextlength',
|
|
6683
|
+
'iv',
|
|
6684
|
+
'nonce',
|
|
6685
|
+
'authtag',
|
|
6686
|
+
'tag',
|
|
6687
|
+
]);
|
|
6688
|
+
|
|
6689
|
+
function assertSecretMetadataOnly(value, pathParts = []) {
|
|
6690
|
+
if (!value || typeof value !== 'object') return;
|
|
6691
|
+
if (Array.isArray(value)) {
|
|
6692
|
+
value.forEach((item, index) =>
|
|
6693
|
+
assertSecretMetadataOnly(item, [...pathParts, String(index)])
|
|
6694
|
+
);
|
|
6695
|
+
return;
|
|
6696
|
+
}
|
|
6697
|
+
for (const [key, child] of Object.entries(value)) {
|
|
6698
|
+
const normalizedKey = key.replace(/[_-]/g, '').toLowerCase();
|
|
6699
|
+
if (SECRET_RESPONSE_FORBIDDEN_KEYS.has(normalizedKey)) {
|
|
6700
|
+
const error = new Error(
|
|
6701
|
+
`APP_SECRET_RESPONSE_CONTRACT_VIOLATION: Secret API 返回了禁止的明文/密文派生字段 ${[...pathParts, key].join('.')}`
|
|
6702
|
+
);
|
|
6703
|
+
error.code = 'APP_SECRET_RESPONSE_CONTRACT_VIOLATION';
|
|
6704
|
+
throw error;
|
|
6705
|
+
}
|
|
6706
|
+
assertSecretMetadataOnly(child, [...pathParts, key]);
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6710
|
+
function outputSecretMetadataOnly(value, flags = {}) {
|
|
6711
|
+
assertSecretMetadataOnly(value);
|
|
6712
|
+
if (flags.json) return writeJson(value);
|
|
6713
|
+
print(JSON.stringify(value, null, 2));
|
|
6714
|
+
}
|
|
6715
|
+
|
|
6716
|
+
function requireExplicitProfile(flags, label) {
|
|
6717
|
+
const profile = String(flags.profile || '').trim();
|
|
6718
|
+
if (!profile) fail(`${label} 必须显式提供 --profile <name>`);
|
|
6719
|
+
return profile;
|
|
6720
|
+
}
|
|
6721
|
+
|
|
6722
|
+
function resolveExactSecretName(positional, flags, label) {
|
|
6723
|
+
const selectors = [
|
|
6724
|
+
...(positional || []),
|
|
6725
|
+
...splitList(flags.only),
|
|
6726
|
+
...splitList(flags.code),
|
|
6727
|
+
]
|
|
6728
|
+
.map(value => String(value || '').trim())
|
|
6729
|
+
.filter(Boolean);
|
|
6730
|
+
const names = unique(selectors);
|
|
6731
|
+
if (names.length !== 1) {
|
|
6732
|
+
fail(`${label} 必须通过位置参数、--only 或 --code 精确选择一个 Secret name`);
|
|
6733
|
+
}
|
|
6734
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(names[0])) {
|
|
6735
|
+
fail(`${label}: Secret name 只能包含字母、数字、点、下划线和中划线,并以字母开头`);
|
|
6736
|
+
}
|
|
6737
|
+
return names[0];
|
|
6738
|
+
}
|
|
6739
|
+
|
|
6740
|
+
function rejectUnsafeSecretValueFlags(flags) {
|
|
6741
|
+
const forbidden = [
|
|
6742
|
+
'value',
|
|
6743
|
+
'value-file',
|
|
6744
|
+
'secret',
|
|
6745
|
+
'secret-file',
|
|
6746
|
+
'json-file',
|
|
6747
|
+
'body-json',
|
|
6748
|
+
].filter(key => flags[key] !== undefined);
|
|
6749
|
+
if (forbidden.length > 0) {
|
|
6750
|
+
fail(
|
|
6751
|
+
`Secret 明文禁止出现在命令行或文件参数中: ${forbidden
|
|
6752
|
+
.map(key => `--${key}`)
|
|
6753
|
+
.join(', ')};请使用隐藏 TTY 输入或 --value-stdin`
|
|
6754
|
+
);
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
|
|
6758
|
+
function readSecretValueFromStdin() {
|
|
6759
|
+
const input = fs.readFileSync(0, 'utf8');
|
|
6760
|
+
return input.replace(/\r?\n$/, '');
|
|
6761
|
+
}
|
|
6762
|
+
|
|
6763
|
+
async function readHiddenSecretValue(promptText = 'Secret value: ') {
|
|
6764
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
6765
|
+
fail('非交互环境必须使用 --value-stdin;不会从参数或 workspace 文件读取 Secret');
|
|
6766
|
+
}
|
|
6767
|
+
return await new Promise((resolve, reject) => {
|
|
6768
|
+
const input = process.stdin;
|
|
6769
|
+
const previousRaw = Boolean(input.isRaw);
|
|
6770
|
+
let value = '';
|
|
6771
|
+
const cleanup = () => {
|
|
6772
|
+
input.off('data', onData);
|
|
6773
|
+
try {
|
|
6774
|
+
input.setRawMode(previousRaw);
|
|
6775
|
+
} catch {
|
|
6776
|
+
// The TTY may have closed while handling Ctrl-C.
|
|
6777
|
+
}
|
|
6778
|
+
input.pause();
|
|
6779
|
+
};
|
|
6780
|
+
const onData = chunk => {
|
|
6781
|
+
const textValue = String(chunk);
|
|
6782
|
+
for (const char of textValue) {
|
|
6783
|
+
if (char === '\u0003') {
|
|
6784
|
+
cleanup();
|
|
6785
|
+
process.stderr.write('\n');
|
|
6786
|
+
reject(new Error('Secret 输入已取消'));
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6789
|
+
if (char === '\r' || char === '\n') {
|
|
6790
|
+
cleanup();
|
|
6791
|
+
process.stderr.write('\n');
|
|
6792
|
+
resolve(value);
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
if (char === '\u007f' || char === '\b') {
|
|
6796
|
+
value = value.slice(0, -1);
|
|
6797
|
+
continue;
|
|
6798
|
+
}
|
|
6799
|
+
value += char;
|
|
6800
|
+
}
|
|
6801
|
+
};
|
|
6802
|
+
process.stderr.write(promptText);
|
|
6803
|
+
input.setEncoding('utf8');
|
|
6804
|
+
input.setRawMode(true);
|
|
6805
|
+
input.resume();
|
|
6806
|
+
input.on('data', onData);
|
|
6807
|
+
});
|
|
6808
|
+
}
|
|
6809
|
+
|
|
6810
|
+
async function readSecretValue(flags, label) {
|
|
6811
|
+
rejectUnsafeSecretValueFlags(flags);
|
|
6812
|
+
const value = flags['value-stdin']
|
|
6813
|
+
? readSecretValueFromStdin()
|
|
6814
|
+
: await readHiddenSecretValue(`${label} value: `);
|
|
6815
|
+
if (!value) fail(`${label}: Secret value 不能为空`);
|
|
6816
|
+
if (Buffer.byteLength(value, 'utf8') > 64 * 1024) {
|
|
6817
|
+
fail(`${label}: Secret value 不能超过 64 KiB`);
|
|
6818
|
+
}
|
|
6819
|
+
registerSensitiveText(value);
|
|
6820
|
+
return value;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
function readSecretExpectedRevision(flags, label, options = {}) {
|
|
6824
|
+
const raw = readStringFlag(flags, 'expected-revision');
|
|
6825
|
+
if (options.create && (raw === '' || raw === undefined)) return 0;
|
|
6826
|
+
if (raw === '' || raw === undefined || !/^\d+$/.test(String(raw))) {
|
|
6827
|
+
fail(`${label} 必须提供 --expected-revision <非负整数>`);
|
|
6828
|
+
}
|
|
6829
|
+
const revision = Number(raw);
|
|
6830
|
+
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
6831
|
+
fail(`${label}: --expected-revision 必须是非负安全整数`);
|
|
6832
|
+
}
|
|
6833
|
+
if (options.create && revision !== 0) {
|
|
6834
|
+
fail(`${label}: create 的 --expected-revision 必须是 0`);
|
|
6835
|
+
}
|
|
6836
|
+
return revision;
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
function secretMutationControl(context) {
|
|
6840
|
+
return {
|
|
6841
|
+
publishLeaseId: context.publishLeaseId,
|
|
6842
|
+
baselineId: context.changeBaselineId,
|
|
6843
|
+
changeId: context.changeId,
|
|
6844
|
+
clientSessionId: context.clientSessionId,
|
|
6845
|
+
};
|
|
6846
|
+
}
|
|
6847
|
+
|
|
6848
|
+
function secretClientOperationId(
|
|
6849
|
+
target,
|
|
6850
|
+
control,
|
|
6851
|
+
name,
|
|
6852
|
+
action,
|
|
6853
|
+
expectedRevision,
|
|
6854
|
+
flags = {}
|
|
6855
|
+
) {
|
|
6856
|
+
const explicit = String(
|
|
6857
|
+
flags['client-operation-id'] || flags['idempotency-key'] || ''
|
|
6858
|
+
).trim();
|
|
6859
|
+
if (explicit) return explicit;
|
|
6860
|
+
return `openxiangda-secret-${canonicalJsonSha256({
|
|
6861
|
+
profile: target.profileName,
|
|
6862
|
+
appType: target.appType,
|
|
6863
|
+
name,
|
|
6864
|
+
action,
|
|
6865
|
+
changeId: control.changeId,
|
|
6866
|
+
clientSessionId: control.clientSessionId,
|
|
6867
|
+
expectedRevision,
|
|
6868
|
+
})}`;
|
|
6869
|
+
}
|
|
6870
|
+
|
|
6871
|
+
async function buildSecretDryRun(config, target, subcommand, name, flags) {
|
|
6872
|
+
return await withReadOnlyHttpGuard('secret --dry-run', async () => {
|
|
6873
|
+
const capabilities = assertAppFunctionSecretCapabilities(
|
|
6874
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
6875
|
+
);
|
|
6876
|
+
const current = name
|
|
6877
|
+
? await requestOptionalWithAuth(
|
|
6878
|
+
config,
|
|
6879
|
+
target.profileName,
|
|
6880
|
+
appSecretApiPath(target, `/${encodeURIComponent(name)}`)
|
|
6881
|
+
)
|
|
6882
|
+
: null;
|
|
6883
|
+
const result = {
|
|
6884
|
+
dryRun: true,
|
|
6885
|
+
command: `secret ${subcommand}`,
|
|
6886
|
+
profile: target.profileName,
|
|
6887
|
+
appType: target.appType,
|
|
6888
|
+
name,
|
|
6889
|
+
changeId: readStringFlag(flags, 'change'),
|
|
6890
|
+
expectedRevision:
|
|
6891
|
+
subcommand === 'create'
|
|
6892
|
+
? readSecretExpectedRevision(flags, 'secret create', { create: true })
|
|
6893
|
+
: readSecretExpectedRevision(flags, `secret ${subcommand}`),
|
|
6894
|
+
current,
|
|
6895
|
+
valueRead: false,
|
|
6896
|
+
writes: 0,
|
|
6897
|
+
capabilities: {
|
|
6898
|
+
contractVersion: capabilities.contractVersion,
|
|
6899
|
+
runtimeContractVersion: capabilities.runtimeContractVersion,
|
|
6900
|
+
backend_release_v2: true,
|
|
6901
|
+
atomic_staged_children_v2: true,
|
|
6902
|
+
},
|
|
6903
|
+
};
|
|
6904
|
+
assertSecretMetadataOnly(result);
|
|
6905
|
+
return result;
|
|
6906
|
+
});
|
|
6907
|
+
}
|
|
6908
|
+
|
|
6909
|
+
async function appSecret(args) {
|
|
6910
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
6911
|
+
const { flags, positional } = parseArgs(rest);
|
|
6912
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
6913
|
+
print([
|
|
6914
|
+
'用法: openxiangda secret list|unused --profile <name> [--json]',
|
|
6915
|
+
' openxiangda secret metadata|references|audit <name> --profile <name> [--json]',
|
|
6916
|
+
' openxiangda secret create <name> --profile <name> --change <id> [--value-stdin] [--description text] [--expires-at ISO] [--dry-run]',
|
|
6917
|
+
' openxiangda secret update <name> --profile <name> --change <id> --expected-revision <n> [--description text] [--status active|disabled] [--expires-at ISO] [--dry-run]',
|
|
6918
|
+
' openxiangda secret rotate <name> --profile <name> --change <id> --expected-revision <n> [--value-stdin] [--reason text] [--dry-run]',
|
|
6919
|
+
' openxiangda secret delete <name> --profile <name> --change <id> --expected-revision <n> --reason text [--dry-run]',
|
|
6920
|
+
'Secret 明文只接受隐藏 TTY 或 stdin;禁止 --value、文件、workspace/state 和明文回读。',
|
|
6921
|
+
].join('\n'));
|
|
6922
|
+
return;
|
|
6923
|
+
}
|
|
6924
|
+
|
|
6925
|
+
const profileName = requireExplicitProfile(flags, `secret ${subcommand || ''}`);
|
|
6926
|
+
const config = loadConfig();
|
|
6927
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
6928
|
+
const readOnlyCommands = new Set([
|
|
6929
|
+
'list',
|
|
6930
|
+
'metadata',
|
|
6931
|
+
'get',
|
|
6932
|
+
'references',
|
|
6933
|
+
'audit',
|
|
6934
|
+
'unused',
|
|
6935
|
+
]);
|
|
6936
|
+
if (readOnlyCommands.has(subcommand)) {
|
|
6937
|
+
assertAppFunctionSecretCapabilities(
|
|
6938
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
6939
|
+
);
|
|
6940
|
+
let pathSuffix = '';
|
|
6941
|
+
if (subcommand === 'unused') pathSuffix = '/unused';
|
|
6942
|
+
else if (subcommand !== 'list') {
|
|
6943
|
+
const name = resolveExactSecretName(positional, flags, `secret ${subcommand}`);
|
|
6944
|
+
pathSuffix = `/${encodeURIComponent(name)}`;
|
|
6945
|
+
if (subcommand === 'references' || subcommand === 'audit') {
|
|
6946
|
+
pathSuffix += `/${subcommand}`;
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
const data = await requestWithAuth(
|
|
6950
|
+
config,
|
|
6951
|
+
target.profileName,
|
|
6952
|
+
appSecretApiPath(target, pathSuffix)
|
|
6953
|
+
);
|
|
6954
|
+
return outputSecretMetadataOnly(data, flags);
|
|
6955
|
+
}
|
|
6956
|
+
|
|
6957
|
+
if (!['create', 'update', 'rotate', 'delete'].includes(subcommand)) {
|
|
6958
|
+
fail('用法: openxiangda secret list|metadata|references|audit|unused|create|update|rotate|delete');
|
|
6959
|
+
}
|
|
6960
|
+
requireExplicitProfile(flags, `secret ${subcommand}`);
|
|
6961
|
+
const changeId = String(readStringFlag(flags, 'change') || '').trim();
|
|
6962
|
+
if (!changeId) fail(`secret ${subcommand} 必须提供 --change <id>`);
|
|
6963
|
+
const name = resolveExactSecretName(positional, flags, `secret ${subcommand}`);
|
|
6964
|
+
rejectUnsafeSecretValueFlags(flags);
|
|
6965
|
+
if (flags['dry-run']) {
|
|
6966
|
+
const result = await buildSecretDryRun(
|
|
6967
|
+
config,
|
|
6968
|
+
target,
|
|
6969
|
+
subcommand,
|
|
6970
|
+
name,
|
|
6971
|
+
flags
|
|
6972
|
+
);
|
|
6973
|
+
return outputSecretMetadataOnly(result, flags);
|
|
6974
|
+
}
|
|
6975
|
+
|
|
6976
|
+
assertAppFunctionSecretCapabilities(
|
|
6977
|
+
await fetchAppFunctionSecretCapabilities(config, target)
|
|
6978
|
+
);
|
|
6979
|
+
const context = await ensureDirectMutationPublishContext(
|
|
6980
|
+
config,
|
|
6981
|
+
target,
|
|
6982
|
+
flags,
|
|
6983
|
+
`secret ${subcommand}`
|
|
6984
|
+
);
|
|
6985
|
+
const control = secretMutationControl(context);
|
|
6986
|
+
try {
|
|
6987
|
+
let method;
|
|
6988
|
+
let pathSuffix;
|
|
6989
|
+
let body;
|
|
6990
|
+
if (subcommand === 'create') {
|
|
6991
|
+
const expectedRevision = readSecretExpectedRevision(
|
|
6992
|
+
flags,
|
|
6993
|
+
'secret create',
|
|
6994
|
+
{ create: true }
|
|
6995
|
+
);
|
|
6996
|
+
const value = await readSecretValue(flags, 'secret create');
|
|
6997
|
+
method = 'POST';
|
|
6998
|
+
pathSuffix = '';
|
|
6999
|
+
body = stripUndefinedValues({
|
|
7000
|
+
...control,
|
|
7001
|
+
name,
|
|
7002
|
+
value,
|
|
7003
|
+
description: flags.description,
|
|
7004
|
+
expiresAt: flags['expires-at'],
|
|
7005
|
+
expectedRevision,
|
|
7006
|
+
idempotencyKey: secretClientOperationId(
|
|
7007
|
+
target,
|
|
7008
|
+
control,
|
|
7009
|
+
name,
|
|
7010
|
+
'create',
|
|
7011
|
+
expectedRevision,
|
|
7012
|
+
flags
|
|
7013
|
+
),
|
|
7014
|
+
});
|
|
7015
|
+
} else if (subcommand === 'update') {
|
|
7016
|
+
method = 'PATCH';
|
|
7017
|
+
pathSuffix = `/${encodeURIComponent(name)}`;
|
|
7018
|
+
body = stripUndefinedValues({
|
|
7019
|
+
...control,
|
|
7020
|
+
description: flags.description,
|
|
7021
|
+
status: flags.status,
|
|
7022
|
+
expiresAt: flags['expires-at'],
|
|
7023
|
+
expectedRevision: readSecretExpectedRevision(flags, 'secret update'),
|
|
7024
|
+
});
|
|
7025
|
+
if (
|
|
7026
|
+
body.description === undefined &&
|
|
7027
|
+
body.status === undefined &&
|
|
7028
|
+
body.expiresAt === undefined
|
|
7029
|
+
) {
|
|
7030
|
+
fail('secret update 至少需要 --description、--status 或 --expires-at');
|
|
7031
|
+
}
|
|
7032
|
+
} else if (subcommand === 'rotate') {
|
|
7033
|
+
const expectedRevision = readSecretExpectedRevision(
|
|
7034
|
+
flags,
|
|
7035
|
+
'secret rotate'
|
|
7036
|
+
);
|
|
7037
|
+
const value = await readSecretValue(flags, 'secret rotate');
|
|
7038
|
+
method = 'POST';
|
|
7039
|
+
pathSuffix = `/${encodeURIComponent(name)}/rotate`;
|
|
7040
|
+
body = stripUndefinedValues({
|
|
7041
|
+
...control,
|
|
7042
|
+
value,
|
|
7043
|
+
expectedRevision,
|
|
7044
|
+
idempotencyKey: secretClientOperationId(
|
|
7045
|
+
target,
|
|
7046
|
+
control,
|
|
7047
|
+
name,
|
|
7048
|
+
'rotate',
|
|
7049
|
+
expectedRevision,
|
|
7050
|
+
flags
|
|
7051
|
+
),
|
|
7052
|
+
reason: flags.reason,
|
|
7053
|
+
});
|
|
7054
|
+
} else {
|
|
7055
|
+
const reason = String(flags.reason || '').trim();
|
|
7056
|
+
if (reason.length < 8) {
|
|
7057
|
+
fail('secret delete 必须提供至少 8 个字符的 --reason');
|
|
7058
|
+
}
|
|
7059
|
+
method = 'POST';
|
|
7060
|
+
pathSuffix = `/${encodeURIComponent(name)}/delete`;
|
|
7061
|
+
body = {
|
|
7062
|
+
...control,
|
|
7063
|
+
expectedRevision: readSecretExpectedRevision(flags, 'secret delete'),
|
|
7064
|
+
reason,
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
7067
|
+
const data = await requestWithAuth(
|
|
7068
|
+
config,
|
|
7069
|
+
target.profileName,
|
|
7070
|
+
appSecretApiPath(target, pathSuffix),
|
|
7071
|
+
{ method, body }
|
|
7072
|
+
);
|
|
7073
|
+
return outputSecretMetadataOnly(data, flags);
|
|
7074
|
+
} catch (error) {
|
|
7075
|
+
const safeMessage = maskText(error?.message || String(error));
|
|
7076
|
+
if (error && typeof error === 'object') {
|
|
7077
|
+
error.message = safeMessage;
|
|
7078
|
+
if (typeof error.stack === 'string') error.stack = maskText(error.stack);
|
|
7079
|
+
throw error;
|
|
7080
|
+
}
|
|
7081
|
+
throw new Error(safeMessage);
|
|
7082
|
+
} finally {
|
|
7083
|
+
clearSensitiveText();
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
|
|
7087
|
+
function parseFunctionTestSecretMappings(flags, declaredRefs) {
|
|
7088
|
+
const raw = flags['secret-from-env'];
|
|
7089
|
+
const declared = new Map(declaredRefs.map(ref => [ref.name, ref]));
|
|
7090
|
+
const mappings = new Map();
|
|
7091
|
+
for (const entry of splitList(raw)) {
|
|
7092
|
+
const separator = entry.indexOf('=');
|
|
7093
|
+
const name = separator > 0 ? entry.slice(0, separator).trim() : '';
|
|
7094
|
+
const envName = separator > 0 ? entry.slice(separator + 1).trim() : '';
|
|
7095
|
+
if (!name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envName)) {
|
|
7096
|
+
fail(
|
|
7097
|
+
`function test --secret-from-env 格式必须为 logical=ENV_NAME: ${entry}`
|
|
7098
|
+
);
|
|
7099
|
+
}
|
|
7100
|
+
if (!declared.has(name)) {
|
|
7101
|
+
const error = new Error(
|
|
7102
|
+
`FUNCTION_SECRET_REF_UNDECLARED: Function manifest 未声明 secretRef ${name}`
|
|
7103
|
+
);
|
|
7104
|
+
error.code = 'FUNCTION_SECRET_REF_UNDECLARED';
|
|
7105
|
+
throw error;
|
|
7106
|
+
}
|
|
7107
|
+
if (mappings.has(name)) {
|
|
7108
|
+
fail(`function test 重复声明 --secret-from-env ${name}`);
|
|
7109
|
+
}
|
|
7110
|
+
mappings.set(name, envName);
|
|
7111
|
+
}
|
|
7112
|
+
for (const ref of declaredRefs) {
|
|
7113
|
+
const envName = mappings.get(ref.name);
|
|
7114
|
+
if (!envName) {
|
|
7115
|
+
if (ref.required) {
|
|
7116
|
+
const error = new Error(
|
|
7117
|
+
`FUNCTION_SECRET_NOT_CONFIGURED: required secretRef ${ref.name} 缺少 --secret-from-env ${ref.name}=ENV_NAME`
|
|
7118
|
+
);
|
|
7119
|
+
error.code = 'FUNCTION_SECRET_NOT_CONFIGURED';
|
|
7120
|
+
throw error;
|
|
7121
|
+
}
|
|
7122
|
+
continue;
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
return mappings;
|
|
7126
|
+
}
|
|
7127
|
+
|
|
7128
|
+
function readFunctionTestSecretsFromEnv(mappings, declaredRefs) {
|
|
7129
|
+
const secrets = {};
|
|
7130
|
+
for (const ref of declaredRefs) {
|
|
7131
|
+
const envName = mappings.get(ref.name);
|
|
7132
|
+
if (!envName) continue;
|
|
7133
|
+
if (!Object.prototype.hasOwnProperty.call(process.env, envName)) {
|
|
7134
|
+
const error = new Error(
|
|
7135
|
+
`FUNCTION_SECRET_NOT_CONFIGURED: 测试父进程未设置环境变量 ${envName}(secretRef ${ref.name})`
|
|
7136
|
+
);
|
|
7137
|
+
error.code = 'FUNCTION_SECRET_NOT_CONFIGURED';
|
|
7138
|
+
throw error;
|
|
7139
|
+
}
|
|
7140
|
+
secrets[ref.name] = String(process.env[envName]);
|
|
7141
|
+
}
|
|
7142
|
+
return secrets;
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
function resolveFunctionTestSource(functionItem, functionCode) {
|
|
7146
|
+
const definition = resolveManifestPlainJson(
|
|
7147
|
+
functionItem,
|
|
7148
|
+
'definitionJson',
|
|
7149
|
+
'definitionFile'
|
|
7150
|
+
);
|
|
7151
|
+
const localPath = definition?.sourceFile?.localPath;
|
|
7152
|
+
if (!localPath) {
|
|
7153
|
+
fail(`Function:${functionCode} 缺少 definitionJson.sourceFile.localPath`);
|
|
7154
|
+
}
|
|
7155
|
+
const fromManifest = path.resolve(functionItem.__dir || process.cwd(), localPath);
|
|
7156
|
+
const fromWorkspace = path.resolve(process.cwd(), localPath);
|
|
7157
|
+
const sourcePath = fs.existsSync(fromManifest) ? fromManifest : fromWorkspace;
|
|
7158
|
+
if (!fs.existsSync(sourcePath)) {
|
|
7159
|
+
fail(`Function:${functionCode} sourceFile 不存在: ${localPath}`);
|
|
7160
|
+
}
|
|
7161
|
+
return { definition, sourcePath };
|
|
7162
|
+
}
|
|
7163
|
+
|
|
7164
|
+
function isolatedAppFunctionTestBuildRunner() {
|
|
7165
|
+
const { builtinModules } = require('module');
|
|
7166
|
+
const fs = require('fs');
|
|
7167
|
+
const path = require('path');
|
|
7168
|
+
const esbuild = require(process.argv[1]);
|
|
7169
|
+
const sourcePath = process.argv[2];
|
|
7170
|
+
const outfile = process.argv[3];
|
|
7171
|
+
const workspaceRoot = process.argv[4];
|
|
7172
|
+
const resolveWorkspaceAlias = importPath => {
|
|
7173
|
+
const raw = path.join(workspaceRoot, 'src', importPath.replace(/^@\//, ''));
|
|
7174
|
+
const candidates = [
|
|
7175
|
+
raw,
|
|
7176
|
+
`${raw}.ts`,
|
|
7177
|
+
`${raw}.tsx`,
|
|
7178
|
+
`${raw}.js`,
|
|
7179
|
+
`${raw}.mjs`,
|
|
7180
|
+
`${raw}.cjs`,
|
|
7181
|
+
path.join(raw, 'index.ts'),
|
|
7182
|
+
path.join(raw, 'index.tsx'),
|
|
7183
|
+
path.join(raw, 'index.js'),
|
|
7184
|
+
];
|
|
7185
|
+
return candidates.find(candidate => fs.existsSync(candidate)) || raw;
|
|
7186
|
+
};
|
|
7187
|
+
const external = Array.from(
|
|
7188
|
+
new Set([...builtinModules, ...builtinModules.map(name => `node:${name}`)])
|
|
7189
|
+
);
|
|
7190
|
+
esbuild
|
|
7191
|
+
.build({
|
|
7192
|
+
entryPoints: [sourcePath],
|
|
7193
|
+
outfile,
|
|
7194
|
+
bundle: true,
|
|
7195
|
+
platform: 'node',
|
|
7196
|
+
format: 'cjs',
|
|
7197
|
+
target: ['node20'],
|
|
7198
|
+
sourcemap: false,
|
|
7199
|
+
logLevel: 'silent',
|
|
7200
|
+
external,
|
|
7201
|
+
plugins: [
|
|
7202
|
+
{
|
|
7203
|
+
name: 'openxiangda-function-test-workspace-alias',
|
|
7204
|
+
setup(build) {
|
|
7205
|
+
build.onResolve({ filter: /^@\// }, args => ({
|
|
7206
|
+
path: resolveWorkspaceAlias(args.path),
|
|
7207
|
+
}));
|
|
7208
|
+
},
|
|
7209
|
+
},
|
|
7210
|
+
],
|
|
7211
|
+
})
|
|
7212
|
+
.catch(error => {
|
|
7213
|
+
process.stderr.write(String(error?.message || error));
|
|
7214
|
+
process.exitCode = 1;
|
|
7215
|
+
});
|
|
7216
|
+
}
|
|
7217
|
+
|
|
7218
|
+
async function buildIsolatedFunctionTestBundle(
|
|
7219
|
+
sourcePath,
|
|
7220
|
+
workspaceRoot,
|
|
7221
|
+
functionCode,
|
|
7222
|
+
tempRoot
|
|
7223
|
+
) {
|
|
7224
|
+
const outfile = path.join(tempRoot, 'bundle', `${functionCode}.cjs`);
|
|
7225
|
+
fs.mkdirSync(path.dirname(outfile), { recursive: true });
|
|
7226
|
+
const build = spawnSync(
|
|
7227
|
+
process.execPath,
|
|
7228
|
+
[
|
|
7229
|
+
'-e',
|
|
7230
|
+
`(${isolatedAppFunctionTestBuildRunner.toString()})()`,
|
|
7231
|
+
require.resolve('esbuild'),
|
|
7232
|
+
sourcePath,
|
|
7233
|
+
outfile,
|
|
7234
|
+
workspaceRoot,
|
|
7235
|
+
],
|
|
7236
|
+
{
|
|
7237
|
+
cwd: tempRoot,
|
|
7238
|
+
env: stripUndefinedValues({
|
|
7239
|
+
PATH: process.env.PATH,
|
|
7240
|
+
NODE_ENV: 'test-build',
|
|
7241
|
+
HOME: tempRoot,
|
|
7242
|
+
USERPROFILE: tempRoot,
|
|
7243
|
+
TMPDIR: tempRoot,
|
|
7244
|
+
}),
|
|
7245
|
+
encoding: 'utf8',
|
|
7246
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
7247
|
+
timeout: 30000,
|
|
7248
|
+
killSignal: 'SIGKILL',
|
|
7249
|
+
}
|
|
7250
|
+
);
|
|
7251
|
+
if (build.error || build.status !== 0 || !fs.existsSync(outfile)) {
|
|
7252
|
+
fail(
|
|
7253
|
+
`App Function 隔离测试构建失败: ${functionCode}: ${
|
|
7254
|
+
build.error?.message || build.stderr || build.stdout || 'unknown error'
|
|
7255
|
+
}`
|
|
7256
|
+
);
|
|
7257
|
+
}
|
|
7258
|
+
return outfile;
|
|
7259
|
+
}
|
|
7260
|
+
|
|
7261
|
+
function isolatedAppFunctionTestRunner() {
|
|
7262
|
+
const fs = require('fs');
|
|
7263
|
+
const payload = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
7264
|
+
const secretValues = Object.values(payload.secrets || {}).map(String);
|
|
7265
|
+
const secretVariants = Array.from(
|
|
7266
|
+
new Set(
|
|
7267
|
+
secretValues.flatMap(secret => {
|
|
7268
|
+
const bytes = Buffer.from(secret, 'utf8');
|
|
7269
|
+
const jsonEscaped = JSON.stringify(secret).slice(1, -1);
|
|
7270
|
+
const base64 = bytes.toString('base64');
|
|
7271
|
+
return [
|
|
7272
|
+
secret,
|
|
7273
|
+
jsonEscaped,
|
|
7274
|
+
encodeURIComponent(secret),
|
|
7275
|
+
base64,
|
|
7276
|
+
base64.replace(/=+$/g, ''),
|
|
7277
|
+
bytes.toString('base64url'),
|
|
7278
|
+
bytes.toString('hex'),
|
|
7279
|
+
].filter(Boolean);
|
|
7280
|
+
})
|
|
7281
|
+
)
|
|
7282
|
+
).sort((left, right) => right.length - left.length);
|
|
7283
|
+
let exfiltrationDetected = false;
|
|
7284
|
+
const containsSecretVariant = (value, seen = new WeakSet()) => {
|
|
7285
|
+
const renderings = [];
|
|
7286
|
+
if (Buffer.isBuffer(value)) {
|
|
7287
|
+
renderings.push(value.toString('utf8'), value.toString('base64'), value.toString('hex'));
|
|
7288
|
+
} else if (typeof value === 'string') {
|
|
7289
|
+
renderings.push(value);
|
|
7290
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
7291
|
+
const bytes = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
7292
|
+
renderings.push(bytes.toString('utf8'), bytes.toString('base64'), bytes.toString('hex'));
|
|
7293
|
+
} else if (!value || typeof value !== 'object') {
|
|
7294
|
+
renderings.push(String(value));
|
|
7295
|
+
}
|
|
7296
|
+
if (
|
|
7297
|
+
renderings.some(rendered =>
|
|
7298
|
+
secretVariants.some(variant => variant && String(rendered).includes(variant))
|
|
7299
|
+
)
|
|
7300
|
+
) {
|
|
7301
|
+
return true;
|
|
7302
|
+
}
|
|
7303
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return false;
|
|
7304
|
+
seen.add(value);
|
|
7305
|
+
const children = Array.isArray(value) ? value : Object.values(value);
|
|
7306
|
+
if (value instanceof Error) {
|
|
7307
|
+
children.push(value.name, value.message, value.stack, value.code, value.cause);
|
|
7308
|
+
}
|
|
7309
|
+
return children.some(child => containsSecretVariant(child, seen));
|
|
7310
|
+
};
|
|
7311
|
+
const redactString = value => {
|
|
7312
|
+
let result = String(value);
|
|
7313
|
+
for (const variant of secretVariants) {
|
|
7314
|
+
if (variant) result = result.split(variant).join('***secret***');
|
|
7315
|
+
}
|
|
7316
|
+
return result;
|
|
7317
|
+
};
|
|
7318
|
+
const sanitize = (value, seen = new WeakSet()) => {
|
|
7319
|
+
if (typeof value === 'string') return redactString(value);
|
|
7320
|
+
if (!value || typeof value !== 'object') return value;
|
|
7321
|
+
if (seen.has(value)) return '[Circular]';
|
|
7322
|
+
seen.add(value);
|
|
7323
|
+
if (Array.isArray(value)) return value.map(item => sanitize(item, seen));
|
|
7324
|
+
return Object.fromEntries(
|
|
7325
|
+
Object.entries(value).map(([key, child]) => [key, sanitize(child, seen)])
|
|
7326
|
+
);
|
|
7327
|
+
};
|
|
7328
|
+
const renderLog = values => {
|
|
7329
|
+
if (values.some(containsSecretVariant)) exfiltrationDetected = true;
|
|
7330
|
+
return redactString(
|
|
7331
|
+
values
|
|
7332
|
+
.map(value =>
|
|
7333
|
+
typeof value === 'string' ? value : JSON.stringify(sanitize(value))
|
|
7334
|
+
)
|
|
7335
|
+
.join(' ')
|
|
7336
|
+
);
|
|
7337
|
+
};
|
|
7338
|
+
const safeConsole = Object.freeze({
|
|
7339
|
+
log: (...values) => process.stderr.write(`${renderLog(values)}\n`),
|
|
7340
|
+
info: (...values) => process.stderr.write(`${renderLog(values)}\n`),
|
|
7341
|
+
warn: (...values) => process.stderr.write(`${renderLog(values)}\n`),
|
|
7342
|
+
error: (...values) => process.stderr.write(`${renderLog(values)}\n`),
|
|
7343
|
+
debug: (...values) => process.stderr.write(`${renderLog(values)}\n`),
|
|
7344
|
+
});
|
|
7345
|
+
global.console = safeConsole;
|
|
7346
|
+
const declaredNames = new Set(
|
|
7347
|
+
(payload.secretRefs || []).map(item => String(item.name || ''))
|
|
7348
|
+
);
|
|
7349
|
+
const secrets = Object.freeze({
|
|
7350
|
+
get: async name => {
|
|
7351
|
+
const normalized = String(name || '');
|
|
7352
|
+
if (!declaredNames.has(normalized)) {
|
|
7353
|
+
const error = new Error(`Function secretRef is not declared: ${normalized}`);
|
|
7354
|
+
error.code = 'FUNCTION_SECRET_REF_UNDECLARED';
|
|
7355
|
+
error.status = 403;
|
|
7356
|
+
throw error;
|
|
7357
|
+
}
|
|
7358
|
+
if (!Object.prototype.hasOwnProperty.call(payload.secrets || {}, normalized)) {
|
|
7359
|
+
const error = new Error(`Function secret is not configured: ${normalized}`);
|
|
7360
|
+
error.code = 'FUNCTION_SECRET_NOT_CONFIGURED';
|
|
7361
|
+
error.status = 424;
|
|
7362
|
+
throw error;
|
|
7363
|
+
}
|
|
7364
|
+
return String(payload.secrets[normalized]);
|
|
7365
|
+
},
|
|
7366
|
+
});
|
|
7367
|
+
const logger = Object.freeze({
|
|
7368
|
+
debug: safeConsole.debug,
|
|
7369
|
+
info: safeConsole.info,
|
|
7370
|
+
warn: safeConsole.warn,
|
|
7371
|
+
error: safeConsole.error,
|
|
7372
|
+
});
|
|
7373
|
+
Promise.resolve()
|
|
7374
|
+
.then(() => {
|
|
7375
|
+
const loaded = require(process.argv[1]);
|
|
7376
|
+
const execute = loaded?.default || loaded;
|
|
7377
|
+
if (typeof execute !== 'function') {
|
|
7378
|
+
const error = new Error('App Function module must export a function');
|
|
7379
|
+
error.code = 'APP_FUNCTION_TEST_EXPORT_INVALID';
|
|
7380
|
+
throw error;
|
|
7381
|
+
}
|
|
7382
|
+
const context = Object.freeze({
|
|
7383
|
+
input: payload.input,
|
|
7384
|
+
secrets,
|
|
7385
|
+
logger,
|
|
7386
|
+
console: safeConsole,
|
|
7387
|
+
});
|
|
7388
|
+
return execute(context, payload.input);
|
|
7389
|
+
})
|
|
7390
|
+
.then(result => {
|
|
7391
|
+
if (containsSecretVariant(result) || exfiltrationDetected) {
|
|
7392
|
+
const error = new Error('App Function attempted to expose a secret');
|
|
7393
|
+
error.code = 'APP_SECRET_EXFILTRATION_BLOCKED';
|
|
7394
|
+
error.status = 500;
|
|
7395
|
+
throw error;
|
|
7396
|
+
}
|
|
7397
|
+
process.stdout.write(
|
|
7398
|
+
JSON.stringify({ ok: true, result: sanitize(result) })
|
|
7399
|
+
);
|
|
7400
|
+
})
|
|
7401
|
+
.catch(error => {
|
|
7402
|
+
const errorCode = String(error?.code || '');
|
|
7403
|
+
const blocked =
|
|
7404
|
+
error?.code === 'APP_SECRET_EXFILTRATION_BLOCKED' ||
|
|
7405
|
+
exfiltrationDetected ||
|
|
7406
|
+
containsSecretVariant(error);
|
|
7407
|
+
const safeErrorCode = /^[A-Z][A-Z0-9_]{0,127}$/.test(errorCode)
|
|
7408
|
+
? errorCode
|
|
7409
|
+
: 'APP_FUNCTION_TEST_FAILED';
|
|
7410
|
+
process.stdout.write(
|
|
7411
|
+
JSON.stringify({
|
|
7412
|
+
ok: false,
|
|
7413
|
+
error: {
|
|
7414
|
+
code: blocked
|
|
7415
|
+
? 'APP_SECRET_EXFILTRATION_BLOCKED'
|
|
7416
|
+
: safeErrorCode,
|
|
7417
|
+
status: blocked ? 500 : error?.status,
|
|
7418
|
+
message: blocked
|
|
7419
|
+
? 'App Function secret exfiltration was blocked'
|
|
7420
|
+
: redactString(error?.message || String(error)),
|
|
7421
|
+
},
|
|
7422
|
+
})
|
|
7423
|
+
);
|
|
7424
|
+
process.exitCode = 1;
|
|
7425
|
+
});
|
|
7426
|
+
}
|
|
7427
|
+
|
|
7428
|
+
async function testAppFunctionLocally(positional, flags = {}) {
|
|
7429
|
+
rejectUnsafeSecretValueFlags(flags);
|
|
7430
|
+
const functionCode = resolveExactSecretName(
|
|
7431
|
+
positional,
|
|
7432
|
+
flags,
|
|
7433
|
+
'function test'
|
|
7434
|
+
);
|
|
7435
|
+
const typeFilters = new Set(['functions']);
|
|
7436
|
+
const codeFilters = [{ key: 'functions', code: functionCode, raw: functionCode }];
|
|
7437
|
+
const manifest = loadWorkspaceResources({ typeFilters, codeFilters });
|
|
7438
|
+
if (
|
|
7439
|
+
flags['stage-only'] &&
|
|
7440
|
+
(manifest.functions || []).length + (manifest.automations || []).length === 0
|
|
7441
|
+
) {
|
|
7442
|
+
fail('--stage-only 没有匹配到可冻结的 Function/Automation 资源');
|
|
7443
|
+
}
|
|
7444
|
+
const validation = validateWorkspaceResources(manifest);
|
|
7445
|
+
if (validation.errors.length > 0) {
|
|
7446
|
+
fail(`Function:${functionCode} 本地 manifest 校验失败: ${validation.errors[0]}`);
|
|
7447
|
+
}
|
|
7448
|
+
const functionItem = manifest.functions?.[0];
|
|
7449
|
+
if (!functionItem) fail(`未找到 Function manifest: ${functionCode}`);
|
|
7450
|
+
const secretRefs = functionManifestDeclaresSecretRefs(functionItem)
|
|
7451
|
+
? normalizeFunctionSecretRefs(functionItem.secretRefs)
|
|
7452
|
+
: [];
|
|
7453
|
+
const secretMappings = parseFunctionTestSecretMappings(flags, secretRefs);
|
|
7454
|
+
const input = flags['input-json']
|
|
7455
|
+
? readJsonArg(flags['input-json'], 'input-json')
|
|
7456
|
+
: {};
|
|
7457
|
+
const { sourcePath } = resolveFunctionTestSource(
|
|
7458
|
+
functionItem,
|
|
7459
|
+
functionCode
|
|
7460
|
+
);
|
|
7461
|
+
const workspaceRoot = findWorkspaceRoot(sourcePath);
|
|
7462
|
+
const tempRoot = fs.mkdtempSync(
|
|
7463
|
+
path.join(os.tmpdir(), 'openxiangda-function-test-')
|
|
7464
|
+
);
|
|
7465
|
+
try {
|
|
7466
|
+
const bundlePath = await buildIsolatedFunctionTestBundle(
|
|
7467
|
+
sourcePath,
|
|
7468
|
+
workspaceRoot,
|
|
7469
|
+
functionCode,
|
|
7470
|
+
tempRoot
|
|
7471
|
+
);
|
|
7472
|
+
const secrets = readFunctionTestSecretsFromEnv(
|
|
7473
|
+
secretMappings,
|
|
7474
|
+
secretRefs
|
|
7475
|
+
);
|
|
7476
|
+
for (const value of Object.values(secrets)) registerSensitiveText(value);
|
|
7477
|
+
const child = spawnSync(
|
|
7478
|
+
process.execPath,
|
|
7479
|
+
['-e', `(${isolatedAppFunctionTestRunner.toString()})()`, bundlePath],
|
|
7480
|
+
{
|
|
7481
|
+
cwd: tempRoot,
|
|
7482
|
+
env: stripUndefinedValues({
|
|
7483
|
+
PATH: process.env.PATH,
|
|
7484
|
+
NODE_ENV: 'test',
|
|
7485
|
+
HOME: tempRoot,
|
|
7486
|
+
USERPROFILE: tempRoot,
|
|
7487
|
+
TMPDIR: tempRoot,
|
|
7488
|
+
}),
|
|
7489
|
+
input: JSON.stringify({ input, secretRefs, secrets }),
|
|
7490
|
+
encoding: 'utf8',
|
|
7491
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
7492
|
+
timeout: Number(flags.timeout || 30000),
|
|
7493
|
+
killSignal: 'SIGKILL',
|
|
7494
|
+
}
|
|
7495
|
+
);
|
|
7496
|
+
if (child.error) {
|
|
7497
|
+
const error = new Error(
|
|
7498
|
+
`APP_FUNCTION_SECRET_UNAVAILABLE: 隔离测试子进程失败: ${child.error.message}`
|
|
7499
|
+
);
|
|
7500
|
+
error.code = 'APP_FUNCTION_SECRET_UNAVAILABLE';
|
|
7501
|
+
throw error;
|
|
7502
|
+
}
|
|
7503
|
+
if (child.stderr) process.stderr.write(maskText(child.stderr));
|
|
7504
|
+
let output;
|
|
7505
|
+
try {
|
|
7506
|
+
output = JSON.parse(String(child.stdout || '{}'));
|
|
7507
|
+
} catch (error) {
|
|
7508
|
+
fail(`App Function 隔离测试输出无法解析: ${error.message}`);
|
|
7509
|
+
}
|
|
7510
|
+
const result = {
|
|
7511
|
+
...output,
|
|
7512
|
+
functionCode,
|
|
7513
|
+
runtimeContractVersion: secretRefs.length
|
|
7514
|
+
? APP_FUNCTION_SECRET_CAPABILITIES.runtimeContractVersion
|
|
7515
|
+
: 'trusted_node_v1_compat',
|
|
7516
|
+
isolated: true,
|
|
7517
|
+
workspaceWrites: 0,
|
|
7518
|
+
secretValuesPersisted: false,
|
|
7519
|
+
};
|
|
7520
|
+
if (flags.json) writeJson(result);
|
|
7521
|
+
else print(JSON.stringify(result, null, 2));
|
|
7522
|
+
if (child.status !== 0 || output.ok === false) {
|
|
7523
|
+
const error = new Error(
|
|
7524
|
+
`${output.error?.code || 'APP_FUNCTION_TEST_FAILED'}: ${output.error?.message || 'App Function local test failed'}`
|
|
7525
|
+
);
|
|
7526
|
+
error.code = output.error?.code || 'APP_FUNCTION_TEST_FAILED';
|
|
7527
|
+
throw error;
|
|
7528
|
+
}
|
|
7529
|
+
return result;
|
|
7530
|
+
} finally {
|
|
7531
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
7532
|
+
clearSensitiveText();
|
|
7533
|
+
}
|
|
7534
|
+
}
|
|
7535
|
+
|
|
7536
|
+
async function connector(args) {
|
|
7537
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
7538
|
+
const { flags, positional } = parseArgs(rest);
|
|
7539
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
7540
|
+
print('用法: openxiangda connector list|get|create|update|upsert|delete|invoke|download-test [connectorCode[.apiCode]] [--json-file file]');
|
|
7541
|
+
return;
|
|
7542
|
+
}
|
|
7543
|
+
const config = loadConfig();
|
|
7544
|
+
const target = getWorkspaceTarget(config, flags.profile || config.currentProfile, flags);
|
|
7545
|
+
|
|
7546
|
+
if (subcommand === 'invoke' || subcommand === 'download-test') {
|
|
7547
|
+
const targetName = positional[0] || flags.connector;
|
|
7548
|
+
if (!targetName) fail(`用法: openxiangda connector ${subcommand} <connectorCode.apiCode> [--body-json file|json]`);
|
|
7549
|
+
const parsed = parseConnectorApiName(targetName, flags);
|
|
7550
|
+
const body = {
|
|
7551
|
+
connector: parsed.connector,
|
|
7552
|
+
api: parsed.api,
|
|
7553
|
+
...readDirectJsonBody(flags, 'connector invoke', { optional: true }),
|
|
7554
|
+
...(flags['path-params-json'] ? { pathParams: readJsonArg(flags['path-params-json'], 'path-params-json') } : {}),
|
|
7555
|
+
...(flags['query-json'] ? { query: readJsonArg(flags['query-json'], 'query-json') } : {}),
|
|
7556
|
+
...(flags['headers-json'] ? { headers: readJsonArg(flags['headers-json'], 'headers-json') } : {}),
|
|
7557
|
+
...(flags['request-body-type'] ? { requestBodyType: flags['request-body-type'] } : {}),
|
|
7558
|
+
...(flags['response-type'] ? { responseType: flags['response-type'] } : {}),
|
|
7559
|
+
};
|
|
7560
|
+
if (subcommand === 'download-test') body.responseType = 'binary';
|
|
7561
|
+
return runDirectRequest(config, target, flags, {
|
|
7562
|
+
method: 'POST',
|
|
7563
|
+
path: `/${encodeURIComponent(target.appType)}/v1/connectors/actions/${subcommand === 'download-test' ? 'download' : 'invoke'}`,
|
|
7564
|
+
body,
|
|
7565
|
+
strictEnvelope: subcommand !== 'download-test',
|
|
7566
|
+
}, { controlPlaneMutation: false });
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7569
|
+
return directResourceCrud(config, target, {
|
|
7570
|
+
commandName: 'connector',
|
|
7571
|
+
subcommand,
|
|
7572
|
+
flags,
|
|
7573
|
+
positional,
|
|
7574
|
+
resourceType: 'connector',
|
|
7575
|
+
basePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors`,
|
|
7576
|
+
manifestDir: path.join('src', 'resources', 'connectors'),
|
|
7577
|
+
createMethod: 'POST',
|
|
7578
|
+
createPath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
7579
|
+
createBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
7580
|
+
updateMethod: 'POST',
|
|
7581
|
+
updatePath: `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/connectors/actions/sync`,
|
|
7582
|
+
updateBody: body => ({ connectors: [normalizeConnectorManifest(body)] }),
|
|
7583
|
+
normalizeBody: body => normalizeConnectorManifest(body),
|
|
7584
|
+
codeOf: body => body.code || body.methodName,
|
|
7585
|
+
responseData: data => Array.isArray(data?.data) ? data.data[0] : data,
|
|
7586
|
+
saveState: (code, data) => saveConnectorResource(target, code, data?.connector?.id || data?.id, {
|
|
7587
|
+
apis: Object.fromEntries(
|
|
7588
|
+
(data?.apis || []).map(api => [api.code || api.methodName, { apiId: api.id, name: api.name }])
|
|
7589
|
+
),
|
|
7590
|
+
}),
|
|
7591
|
+
});
|
|
7592
|
+
}
|
|
7593
|
+
|
|
7594
|
+
async function notification(args) {
|
|
7595
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
7596
|
+
const { flags, positional } = parseArgs(rest);
|
|
6184
7597
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
6185
7598
|
print([
|
|
6186
7599
|
'用法: openxiangda notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send|capabilities|dingding-preview|dingding-send',
|
|
@@ -7242,12 +8655,14 @@ async function resource(args) {
|
|
|
7242
8655
|
' openxiangda resource validate --profile <name>',
|
|
7243
8656
|
' openxiangda resource plan [type] --only <code1,code2> --profile <name> --json',
|
|
7244
8657
|
' openxiangda resource publish [type] --only <code1,code2> --profile <name>',
|
|
8658
|
+
' openxiangda resource publish function --only <code> --stage-only --profile <name>',
|
|
7245
8659
|
' openxiangda resource publish form-settings --only <code> --activate --profile <name>',
|
|
7246
8660
|
' openxiangda resource publish [type] --all --reason "approved app-wide closure" --profile <name>',
|
|
7247
8661
|
'说明:',
|
|
7248
8662
|
' - 读取 src/resources/**;workflowFile 会先走非写入编译校验。',
|
|
7249
8663
|
' - publish 默认跳过 plan 判定为 noop 的资源,避免无意义 upsert;需要强制写入时加 --force。',
|
|
7250
8664
|
' - Form settings bundle 默认只 stage,不推进线上 Form head;只有显式 --activate 才 CAS 激活。',
|
|
8665
|
+
' - Function/Automation 可用 --stage-only 只 prepare + verify Backend Release;随后把返回的 stagedResource 交给 release app-finalize 原子激活。',
|
|
7251
8666
|
' - publish --dry-run 等价于发布前计划,不会写平台资源。',
|
|
7252
8667
|
' - 在线 publish 必须用 --only/--code 精确选择;真正全量发布必须显式 --all --reason,SDD bypass 不能绕过此范围门。',
|
|
7253
8668
|
' - 源码依赖触发的 Function/Automation 更新默认只替换 source snapshot,保留线上 bindings/schema/metadata;显式替换 manifest 必须加 --replace-manifest --reason。',
|
|
@@ -7284,6 +8699,7 @@ async function resource(args) {
|
|
|
7284
8699
|
|
|
7285
8700
|
const typeFilters = getResourceTypeFilters(positional, flags);
|
|
7286
8701
|
const codeFilters = getResourceCodeFilters(flags);
|
|
8702
|
+
assertBackendStageOnlyScope(subcommand, flags, typeFilters);
|
|
7287
8703
|
assertExplicitResourcePublishScope(subcommand, flags, codeFilters);
|
|
7288
8704
|
assertExplicitManifestReplacement(flags);
|
|
7289
8705
|
if (codeFilters && flags.prune) {
|
|
@@ -7353,6 +8769,19 @@ async function resource(args) {
|
|
|
7353
8769
|
const publishPlan = await telemetry.runPhase('plan', async () =>
|
|
7354
8770
|
buildResourcePlan(config, target, manifest)
|
|
7355
8771
|
);
|
|
8772
|
+
if (publishPlan.functionSecretRefs?.valid === false) {
|
|
8773
|
+
const missing = publishPlan.functionSecretRefs.functions
|
|
8774
|
+
.flatMap(item =>
|
|
8775
|
+
item.missingRequired.map(ref => `${item.code}:${ref.name}`)
|
|
8776
|
+
)
|
|
8777
|
+
.sort();
|
|
8778
|
+
const error = new Error(
|
|
8779
|
+
`FUNCTION_SECRET_NOT_CONFIGURED: 以下 Function required secretRefs 尚未配置或未激活: ${missing.join(', ')}`
|
|
8780
|
+
);
|
|
8781
|
+
error.code = 'FUNCTION_SECRET_NOT_CONFIGURED';
|
|
8782
|
+
error.missingSecretRefs = missing;
|
|
8783
|
+
throw error;
|
|
8784
|
+
}
|
|
7356
8785
|
const pruneCandidates = flags.prune
|
|
7357
8786
|
? await telemetry.runPhase('prune-plan', async () =>
|
|
7358
8787
|
collectPruneResourceCandidates(config, target, manifest)
|
|
@@ -7381,8 +8810,16 @@ async function resource(args) {
|
|
|
7381
8810
|
resourceTargets,
|
|
7382
8811
|
{
|
|
7383
8812
|
files: scopedFiles,
|
|
7384
|
-
commands: [
|
|
8813
|
+
commands: [
|
|
8814
|
+
buildResourcePublishCommand(
|
|
8815
|
+
typeFilters,
|
|
8816
|
+
codeFilters,
|
|
8817
|
+
profileName,
|
|
8818
|
+
flags
|
|
8819
|
+
),
|
|
8820
|
+
],
|
|
7385
8821
|
changeId: flags.change,
|
|
8822
|
+
actualCommands: true,
|
|
7386
8823
|
}
|
|
7387
8824
|
),
|
|
7388
8825
|
flags,
|
|
@@ -7426,11 +8863,27 @@ async function resource(args) {
|
|
|
7426
8863
|
baseRef: flags.since || 'HEAD',
|
|
7427
8864
|
replaceManifest: Boolean(flags['replace-manifest']),
|
|
7428
8865
|
activateFormBundles: Boolean(flags.activate),
|
|
8866
|
+
stageBackendReleaseOnly: Boolean(flags['stage-only']),
|
|
7429
8867
|
})
|
|
7430
8868
|
);
|
|
7431
8869
|
if (changeBaselinePreflight) {
|
|
7432
8870
|
result.changeBaselinePreflight = changeBaselinePreflight;
|
|
7433
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
|
+
}
|
|
7434
8887
|
result.timings = telemetry.stop();
|
|
7435
8888
|
if (flags.json) return writeJson(result);
|
|
7436
8889
|
printResourceResult(result);
|
|
@@ -7439,6 +8892,23 @@ async function resource(args) {
|
|
|
7439
8892
|
}
|
|
7440
8893
|
}
|
|
7441
8894
|
|
|
8895
|
+
function assertBackendStageOnlyScope(subcommand, flags, typeFilters) {
|
|
8896
|
+
if (!flags['stage-only']) return;
|
|
8897
|
+
if (subcommand !== 'publish' || flags['dry-run']) {
|
|
8898
|
+
fail('--stage-only 仅适用于在线 resource publish');
|
|
8899
|
+
}
|
|
8900
|
+
if (flags.prune || flags.activate) {
|
|
8901
|
+
fail('--stage-only 不能与 --prune 或 --activate 同时使用');
|
|
8902
|
+
}
|
|
8903
|
+
const normalized = Array.from(typeFilters || []);
|
|
8904
|
+
if (
|
|
8905
|
+
normalized.length === 0 ||
|
|
8906
|
+
normalized.some(type => !['functions', 'automations'].includes(type))
|
|
8907
|
+
) {
|
|
8908
|
+
fail('--stage-only 必须显式选择 function 或 automation 资源类型');
|
|
8909
|
+
}
|
|
8910
|
+
}
|
|
8911
|
+
|
|
7442
8912
|
function assertExplicitResourcePublishScope(subcommand, flags, codeFilters) {
|
|
7443
8913
|
if (subcommand !== 'publish' || flags['dry-run']) return;
|
|
7444
8914
|
if (Array.isArray(codeFilters) && codeFilters.length > 0) return;
|
|
@@ -7470,7 +8940,12 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
|
|
|
7470
8940
|
.map(action => action.code)
|
|
7471
8941
|
.filter(Boolean)
|
|
7472
8942
|
).sort();
|
|
7473
|
-
const codeResourceKeys = new Set([
|
|
8943
|
+
const codeResourceKeys = new Set([
|
|
8944
|
+
'functions',
|
|
8945
|
+
'automations',
|
|
8946
|
+
'workflows',
|
|
8947
|
+
'formSettings',
|
|
8948
|
+
]);
|
|
7474
8949
|
const hasGenericResources = RESOURCE_SPECS.some(
|
|
7475
8950
|
spec => !codeResourceKeys.has(spec.key) && (manifest[spec.key] || []).length > 0
|
|
7476
8951
|
) || (planActions || []).some(
|
|
@@ -7479,7 +8954,7 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
|
|
|
7479
8954
|
!['function', 'automation', 'workflow'].includes(action.kind)
|
|
7480
8955
|
);
|
|
7481
8956
|
return {
|
|
7482
|
-
forms:
|
|
8957
|
+
forms: codes('formSettings'),
|
|
7483
8958
|
pages: [],
|
|
7484
8959
|
functions: unique([...codes('functions'), ...deletedCodes('function')]).sort(),
|
|
7485
8960
|
automations: unique([
|
|
@@ -7494,20 +8969,51 @@ function buildResourceManifestSddTargets(manifest, planActions = []) {
|
|
|
7494
8969
|
};
|
|
7495
8970
|
}
|
|
7496
8971
|
|
|
7497
|
-
function buildResourcePublishCommand(
|
|
8972
|
+
function buildResourcePublishCommand(
|
|
8973
|
+
typeFilters,
|
|
8974
|
+
codeFilters,
|
|
8975
|
+
profileName,
|
|
8976
|
+
flags = {}
|
|
8977
|
+
) {
|
|
7498
8978
|
const args = ['openxiangda resource publish'];
|
|
7499
|
-
if (typeFilters?.size
|
|
7500
|
-
args.push(Array.from(typeFilters)
|
|
8979
|
+
if (typeFilters?.size > 0) {
|
|
8980
|
+
args.push(Array.from(typeFilters).sort().join(','));
|
|
7501
8981
|
}
|
|
7502
8982
|
if (codeFilters?.length) {
|
|
8983
|
+
const qualifierAliases = {
|
|
8984
|
+
functions: 'function',
|
|
8985
|
+
automations: 'automation',
|
|
8986
|
+
workflows: 'workflow',
|
|
8987
|
+
formSettings: 'form-setting',
|
|
8988
|
+
};
|
|
7503
8989
|
const selectors = codeFilters.map(selector =>
|
|
7504
8990
|
(selector.key || selector.qualifier) && (!typeFilters || typeFilters.size !== 1)
|
|
7505
|
-
? `${
|
|
8991
|
+
? `${
|
|
8992
|
+
qualifierAliases[selector.key || selector.qualifier] ||
|
|
8993
|
+
selector.key ||
|
|
8994
|
+
selector.qualifier
|
|
8995
|
+
}:${selector.code}`
|
|
7506
8996
|
: selector.code
|
|
7507
8997
|
);
|
|
7508
8998
|
args.push(`--only ${Array.from(new Set(selectors)).join(',')}`);
|
|
7509
8999
|
}
|
|
7510
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');
|
|
7511
9017
|
return args.join(' ');
|
|
7512
9018
|
}
|
|
7513
9019
|
|
|
@@ -7615,9 +9121,10 @@ async function runtime(args) {
|
|
|
7615
9121
|
commands: [
|
|
7616
9122
|
`openxiangda runtime deploy --profile ${target.profileName}${
|
|
7617
9123
|
flags.change ? ` --change ${flags.change}` : ''
|
|
7618
|
-
}`,
|
|
9124
|
+
}${flags['no-activate'] ? ' --no-activate' : ''}`,
|
|
7619
9125
|
],
|
|
7620
9126
|
changeId: flags.change,
|
|
9127
|
+
actualCommands: true,
|
|
7621
9128
|
}
|
|
7622
9129
|
),
|
|
7623
9130
|
flags,
|
|
@@ -7854,7 +9361,46 @@ async function runtime(args) {
|
|
|
7854
9361
|
}
|
|
7855
9362
|
)
|
|
7856
9363
|
);
|
|
7857
|
-
|
|
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;
|
|
7858
9404
|
const releaseAssetBaseUrl = data?.assetBaseUrl || assetBaseUrl;
|
|
7859
9405
|
const result = {
|
|
7860
9406
|
appType: target.appType,
|
|
@@ -7876,6 +9422,8 @@ async function runtime(args) {
|
|
|
7876
9422
|
releaseFiles.find(file => file.storageProvider)?.storageProvider ||
|
|
7877
9423
|
null,
|
|
7878
9424
|
traceId,
|
|
9425
|
+
...(stagedResource ? { stagedResource } : {}),
|
|
9426
|
+
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
7879
9427
|
timings: telemetry.stop(),
|
|
7880
9428
|
};
|
|
7881
9429
|
if (flags.json) return writeJson(result);
|
|
@@ -7888,6 +9436,9 @@ async function runtime(args) {
|
|
|
7888
9436
|
`traceId: ${traceId}`,
|
|
7889
9437
|
`assetBase: ${releaseAssetBaseUrl}`,
|
|
7890
9438
|
`active: ${result.activated ? 'yes' : 'no'}`,
|
|
9439
|
+
...(stagedResourcesFile
|
|
9440
|
+
? [`staged resources: ${stagedResourcesFile}`]
|
|
9441
|
+
: []),
|
|
7891
9442
|
].join('\n')
|
|
7892
9443
|
);
|
|
7893
9444
|
return;
|
|
@@ -8368,6 +9919,21 @@ function saveRuntimeReleaseState(target, release) {
|
|
|
8368
9919
|
saveProjectState(target.state);
|
|
8369
9920
|
}
|
|
8370
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
|
+
|
|
8371
9937
|
function formatBytes(value) {
|
|
8372
9938
|
const bytes = Number(value || 0);
|
|
8373
9939
|
if (bytes < 1024) return `${bytes}B`;
|
|
@@ -8493,7 +10059,8 @@ async function commands(args) {
|
|
|
8493
10059
|
'route list|get|create|update|upsert|delete',
|
|
8494
10060
|
'public-access list|get|create|update|upsert|delete|ticket-create|session-test|grant-check',
|
|
8495
10061
|
'auth-config list|get|create|update|upsert|delete|methods',
|
|
8496
|
-
'function list|get|create|update|upsert|delete|invoke',
|
|
10062
|
+
'function list|get|create|update|upsert|delete|invoke|test',
|
|
10063
|
+
'secret list|metadata|references|audit|unused|create|update|rotate|delete',
|
|
8497
10064
|
'connector list|get|create|update|upsert|delete|invoke|download-test',
|
|
8498
10065
|
'notification template-list|template-get|template-upsert|template-delete|type-list|type-get|type-upsert|type-delete|preview|send|batch-send',
|
|
8499
10066
|
'organization capabilities|department-list|department-get|department-create|department-update|account-list|account-get|account-create|account-update|account-reset-password',
|
|
@@ -8514,6 +10081,7 @@ async function commands(args) {
|
|
|
8514
10081
|
'public-access is the new React SPA public policy resource for /view/:appType/public/* routes; page code should use PublicAccessGate, and Page SDK hooks must stay inside OpenXiangdaProvider + OpenXiangdaPageProvider.',
|
|
8515
10082
|
'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
|
|
8516
10083
|
'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
|
|
10084
|
+
'App Function Secret values never belong in a manifest or workspace file. Use top-level function secretRefs, ctx.secrets.get(name), and secret create/rotate via hidden TTY or --value-stdin.',
|
|
8517
10085
|
'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change with coverage.json/release.json verification unless --sdd-bypass --reason is used.',
|
|
8518
10086
|
'A formal release freezes the clean publish HEAD separately from the change base; after activation merge/fast-forward and push that exact SHA to the authoritative default branch, verify release integration-status, then run release end.',
|
|
8519
10087
|
],
|
|
@@ -10430,7 +11998,9 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
10430
11998
|
if (!item.definitionJson && !item.definitionFile) {
|
|
10431
11999
|
errors.push(`${label}: 缺少 definitionJson 或 definitionFile`);
|
|
10432
12000
|
}
|
|
12001
|
+
validateFunctionSecretRefs(label, item, errors);
|
|
10433
12002
|
validateFunctionRuntimeInvoke(label, item, errors, warnings);
|
|
12003
|
+
validateFunctionPerformancePolicies(label, item, errors);
|
|
10434
12004
|
}
|
|
10435
12005
|
if (kind === 'dataViews') {
|
|
10436
12006
|
const definition = item.definition || item;
|
|
@@ -10743,6 +12313,62 @@ function validateFunctionRuntimeInvoke(label, item, errors, warnings) {
|
|
|
10743
12313
|
}
|
|
10744
12314
|
}
|
|
10745
12315
|
|
|
12316
|
+
function validateFunctionPerformancePolicies(label, item, errors) {
|
|
12317
|
+
const definition = item.definitionJson || {};
|
|
12318
|
+
const responseCache = definition.responseCache;
|
|
12319
|
+
if (responseCache !== undefined) {
|
|
12320
|
+
if (!responseCache || typeof responseCache !== 'object' || Array.isArray(responseCache)) {
|
|
12321
|
+
errors.push(`${label}: responseCache 必须是对象`);
|
|
12322
|
+
} else {
|
|
12323
|
+
if (responseCache.enabled !== undefined && typeof responseCache.enabled !== 'boolean') {
|
|
12324
|
+
errors.push(`${label}: responseCache.enabled 必须是 boolean`);
|
|
12325
|
+
}
|
|
12326
|
+
const ttlSeconds = Number(responseCache.ttlSeconds);
|
|
12327
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > 86400) {
|
|
12328
|
+
errors.push(`${label}: responseCache.ttlSeconds 必须是 1-86400 的整数`);
|
|
12329
|
+
}
|
|
12330
|
+
if (!Array.isArray(responseCache.actions) || responseCache.actions.length === 0) {
|
|
12331
|
+
errors.push(`${label}: responseCache.actions 必须是非空数组`);
|
|
12332
|
+
}
|
|
12333
|
+
if (responseCache.scope !== 'public_policy') {
|
|
12334
|
+
errors.push(`${label}: responseCache.scope 当前仅支持 public_policy`);
|
|
12335
|
+
}
|
|
12336
|
+
if (
|
|
12337
|
+
responseCache.invalidateOnForms !== undefined &&
|
|
12338
|
+
!Array.isArray(responseCache.invalidateOnForms)
|
|
12339
|
+
) {
|
|
12340
|
+
errors.push(`${label}: responseCache.invalidateOnForms 必须是数组`);
|
|
12341
|
+
}
|
|
12342
|
+
if (responseCache.maxValueBytes !== undefined) {
|
|
12343
|
+
const maxValueBytes = Number(responseCache.maxValueBytes);
|
|
12344
|
+
if (!Number.isInteger(maxValueBytes) || maxValueBytes < 1024 || maxValueBytes > 4194304) {
|
|
12345
|
+
errors.push(`${label}: responseCache.maxValueBytes 必须是 1024-4194304 的整数`);
|
|
12346
|
+
}
|
|
12347
|
+
}
|
|
12348
|
+
}
|
|
12349
|
+
}
|
|
12350
|
+
|
|
12351
|
+
const invocationAudit = definition.invocationAudit;
|
|
12352
|
+
if (invocationAudit !== undefined) {
|
|
12353
|
+
if (!invocationAudit || typeof invocationAudit !== 'object' || Array.isArray(invocationAudit)) {
|
|
12354
|
+
errors.push(`${label}: invocationAudit 必须是对象`);
|
|
12355
|
+
} else {
|
|
12356
|
+
if (
|
|
12357
|
+
invocationAudit.successOutput !== undefined &&
|
|
12358
|
+
!['full', 'summary'].includes(String(invocationAudit.successOutput))
|
|
12359
|
+
) {
|
|
12360
|
+
errors.push(`${label}: invocationAudit.successOutput 只能是 full 或 summary`);
|
|
12361
|
+
}
|
|
12362
|
+
if (invocationAudit.maxLogBytes !== undefined) {
|
|
12363
|
+
const maxLogBytes = Number(invocationAudit.maxLogBytes);
|
|
12364
|
+
if (!Number.isInteger(maxLogBytes) || maxLogBytes < 1024 || maxLogBytes > 65536) {
|
|
12365
|
+
errors.push(`${label}: invocationAudit.maxLogBytes 必须是 1024-65536 的整数`);
|
|
12366
|
+
}
|
|
12367
|
+
}
|
|
12368
|
+
}
|
|
12369
|
+
}
|
|
12370
|
+
}
|
|
12371
|
+
|
|
10746
12372
|
function validateFunctionRuntimeInvokeAudience(label, value, errors) {
|
|
10747
12373
|
if (Array.isArray(value)) {
|
|
10748
12374
|
value.forEach(item => validateFunctionRuntimeInvokeAudience(label, item, errors));
|
|
@@ -11412,6 +13038,11 @@ async function publishResourcesForWorkspace(config, profileName, options = {}) {
|
|
|
11412
13038
|
|
|
11413
13039
|
async function buildResourcePlan(config, target, manifest) {
|
|
11414
13040
|
return await withReadOnlyHttpGuard('resource plan', async () => {
|
|
13041
|
+
const secretCapabilities = await assertManifestAppFunctionSecretCapabilities(
|
|
13042
|
+
config,
|
|
13043
|
+
target,
|
|
13044
|
+
manifest
|
|
13045
|
+
);
|
|
11415
13046
|
await prepareManifestJsCodeBundlesForPlan(manifest);
|
|
11416
13047
|
const existing = await fetchExistingResourceMaps(config, target, manifest);
|
|
11417
13048
|
const actions = [];
|
|
@@ -11438,6 +13069,14 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
11438
13069
|
actions,
|
|
11439
13070
|
manifest.formSettings || []
|
|
11440
13071
|
);
|
|
13072
|
+
const functionSecretRefs = secretCapabilities
|
|
13073
|
+
? await buildAppFunctionSecretRefPlan(
|
|
13074
|
+
config,
|
|
13075
|
+
target,
|
|
13076
|
+
manifest.functions || [],
|
|
13077
|
+
existing.functions
|
|
13078
|
+
)
|
|
13079
|
+
: null;
|
|
11441
13080
|
return {
|
|
11442
13081
|
appType: target.appType,
|
|
11443
13082
|
profile: target.profileName,
|
|
@@ -11445,10 +13084,99 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
11445
13084
|
...(manifest.resourceCodeFilters ? { resourceCodeFilters: manifest.resourceCodeFilters } : {}),
|
|
11446
13085
|
actions,
|
|
11447
13086
|
summary: summarizeActions(actions),
|
|
13087
|
+
...(functionSecretRefs
|
|
13088
|
+
? {
|
|
13089
|
+
capabilities: {
|
|
13090
|
+
app_function_secrets_v1: true,
|
|
13091
|
+
trusted_node_v2: true,
|
|
13092
|
+
backend_release_v2: true,
|
|
13093
|
+
atomic_staged_children_v2: true,
|
|
13094
|
+
},
|
|
13095
|
+
functionSecretRefs,
|
|
13096
|
+
}
|
|
13097
|
+
: {}),
|
|
11448
13098
|
};
|
|
11449
13099
|
});
|
|
11450
13100
|
}
|
|
11451
13101
|
|
|
13102
|
+
async function buildAppFunctionSecretRefPlan(
|
|
13103
|
+
config,
|
|
13104
|
+
target,
|
|
13105
|
+
functions,
|
|
13106
|
+
existingFunctions
|
|
13107
|
+
) {
|
|
13108
|
+
const declaredFunctions = (functions || []).filter(
|
|
13109
|
+
functionManifestDeclaresSecretRefs
|
|
13110
|
+
);
|
|
13111
|
+
if (declaredFunctions.length === 0) return null;
|
|
13112
|
+
const metadataResponse = await requestWithAuth(
|
|
13113
|
+
config,
|
|
13114
|
+
target.profileName,
|
|
13115
|
+
appSecretApiPath(target)
|
|
13116
|
+
);
|
|
13117
|
+
assertSecretMetadataOnly(metadataResponse);
|
|
13118
|
+
const metadataByName = new Map(
|
|
13119
|
+
normalizeItems(metadataResponse)
|
|
13120
|
+
.filter(item => item?.name)
|
|
13121
|
+
.map(item => [String(item.name), item])
|
|
13122
|
+
);
|
|
13123
|
+
const plans = declaredFunctions.map(item => {
|
|
13124
|
+
const code = item.code || item.functionCode || item.resourceCode;
|
|
13125
|
+
const desired = normalizeFunctionSecretRefs(item.secretRefs);
|
|
13126
|
+
const current = normalizeFunctionSecretRefs(
|
|
13127
|
+
existingFunctions?.get(code)?.secretRefs ??
|
|
13128
|
+
existingFunctions?.get(code)?.secretReferences ??
|
|
13129
|
+
[]
|
|
13130
|
+
);
|
|
13131
|
+
const desiredNames = new Set(desired.map(ref => ref.name));
|
|
13132
|
+
const currentNames = new Set(current.map(ref => ref.name));
|
|
13133
|
+
const added = desired
|
|
13134
|
+
.map(ref => ref.name)
|
|
13135
|
+
.filter(name => !currentNames.has(name));
|
|
13136
|
+
const removed = current
|
|
13137
|
+
.map(ref => ref.name)
|
|
13138
|
+
.filter(name => !desiredNames.has(name));
|
|
13139
|
+
const missingRequired = [];
|
|
13140
|
+
const unavailableOptional = [];
|
|
13141
|
+
for (const ref of desired) {
|
|
13142
|
+
const metadata = metadataByName.get(ref.name);
|
|
13143
|
+
let reason = null;
|
|
13144
|
+
if (!metadata) reason = 'missing';
|
|
13145
|
+
else if (String(metadata.status || 'active') !== 'active') {
|
|
13146
|
+
reason = `status:${metadata.status || 'unknown'}`;
|
|
13147
|
+
} else if (metadata.hasValue !== true) {
|
|
13148
|
+
reason = 'value-not-configured';
|
|
13149
|
+
}
|
|
13150
|
+
if (!reason) continue;
|
|
13151
|
+
(ref.required ? missingRequired : unavailableOptional).push({
|
|
13152
|
+
name: ref.name,
|
|
13153
|
+
reason,
|
|
13154
|
+
});
|
|
13155
|
+
}
|
|
13156
|
+
const unusedAfterActivation = removed.filter(name => {
|
|
13157
|
+
const referenceCount = Number(metadataByName.get(name)?.referenceCount);
|
|
13158
|
+
return Number.isFinite(referenceCount) && referenceCount <= 1;
|
|
13159
|
+
});
|
|
13160
|
+
return {
|
|
13161
|
+
code,
|
|
13162
|
+
desired,
|
|
13163
|
+
current,
|
|
13164
|
+
added,
|
|
13165
|
+
removed,
|
|
13166
|
+
missingRequired,
|
|
13167
|
+
unavailableOptional,
|
|
13168
|
+
unusedAfterActivation,
|
|
13169
|
+
};
|
|
13170
|
+
});
|
|
13171
|
+
return {
|
|
13172
|
+
contractVersion: APP_FUNCTION_SECRET_CAPABILITIES.contractVersion,
|
|
13173
|
+
runtimeContractVersion:
|
|
13174
|
+
APP_FUNCTION_SECRET_CAPABILITIES.runtimeContractVersion,
|
|
13175
|
+
functions: plans,
|
|
13176
|
+
valid: plans.every(item => item.missingRequired.length === 0),
|
|
13177
|
+
};
|
|
13178
|
+
}
|
|
13179
|
+
|
|
11452
13180
|
async function prepareManifestJsCodeBundlesForPlan(manifest) {
|
|
11453
13181
|
const targetsByWorkspace = new Map();
|
|
11454
13182
|
const seenObjects = new Set();
|
|
@@ -11649,19 +13377,19 @@ function collectBackendReleaseCandidates(manifest, options = {}) {
|
|
|
11649
13377
|
for (const item of items || []) {
|
|
11650
13378
|
const code = item.code || item.functionCode || item.resourceCode;
|
|
11651
13379
|
const action = getPlannedResourceAction(options, planKind, code);
|
|
11652
|
-
if (!action || !['update', 'noop'].includes(action.action)) continue;
|
|
13380
|
+
if (!action || !['create', 'update', 'noop'].includes(action.action)) continue;
|
|
11653
13381
|
if (action.action === 'noop' && options.skipNoop !== false) continue;
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
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
|
+
});
|
|
11665
13393
|
}
|
|
11666
13394
|
};
|
|
11667
13395
|
append('Function', 'functions', manifest.functions);
|
|
@@ -11833,25 +13561,206 @@ async function buildBackendReleaseResourceInput(
|
|
|
11833
13561
|
candidate.item,
|
|
11834
13562
|
definitionJson
|
|
11835
13563
|
);
|
|
11836
|
-
|
|
13564
|
+
const secretRefsDeclared = functionManifestDeclaresSecretRefs(
|
|
13565
|
+
candidate.item
|
|
13566
|
+
);
|
|
13567
|
+
const common = {
|
|
11837
13568
|
kind: 'Function',
|
|
11838
13569
|
code: candidate.code,
|
|
11839
|
-
|
|
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
|
+
}
|
|
13589
|
+
const source = backendSourcePlane(
|
|
13590
|
+
definitionJson,
|
|
13591
|
+
`Function:${candidate.code}`
|
|
13592
|
+
);
|
|
13593
|
+
if (secretRefsDeclared) {
|
|
13594
|
+
source.version = definitionJson.version;
|
|
13595
|
+
source.runtimeContractVersion = definitionJson.runtimeContractVersion;
|
|
13596
|
+
}
|
|
13597
|
+
return {
|
|
13598
|
+
...common,
|
|
13599
|
+
...(candidate.operation === 'create'
|
|
13600
|
+
? {
|
|
13601
|
+
create: buildBackendReleaseFunctionCreateInput(
|
|
13602
|
+
candidate.item,
|
|
13603
|
+
definitionJson
|
|
13604
|
+
),
|
|
13605
|
+
}
|
|
13606
|
+
: {}),
|
|
13607
|
+
source,
|
|
13608
|
+
};
|
|
13609
|
+
}
|
|
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 = {
|
|
13630
|
+
kind: 'Automation',
|
|
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,
|
|
11840
13641
|
definitionJson,
|
|
11841
|
-
|
|
13642
|
+
viewJson
|
|
11842
13643
|
),
|
|
11843
13644
|
};
|
|
11844
13645
|
}
|
|
11845
13646
|
return {
|
|
11846
|
-
|
|
11847
|
-
code: candidate.code,
|
|
13647
|
+
...common,
|
|
11848
13648
|
sources: buildAutomationBackendReleaseSources(
|
|
11849
13649
|
definitionJson,
|
|
11850
13650
|
`Automation:${candidate.code}`
|
|
11851
13651
|
),
|
|
13652
|
+
...(candidate.operation === 'create'
|
|
13653
|
+
? {
|
|
13654
|
+
create: buildBackendReleaseAutomationMetadata(
|
|
13655
|
+
target,
|
|
13656
|
+
candidate.item,
|
|
13657
|
+
withoutBackendSourcePlanes(definitionJson),
|
|
13658
|
+
viewJson
|
|
13659
|
+
),
|
|
13660
|
+
}
|
|
13661
|
+
: {}),
|
|
11852
13662
|
};
|
|
11853
13663
|
}
|
|
11854
13664
|
|
|
13665
|
+
function buildBackendReleaseFunctionCreateInput(functionItem, definitionJson) {
|
|
13666
|
+
const baseDefinition = clonePlainJson(definitionJson || {});
|
|
13667
|
+
for (const key of [
|
|
13668
|
+
'sourceFile',
|
|
13669
|
+
'sourceType',
|
|
13670
|
+
'sourceProvenance',
|
|
13671
|
+
'secretRefs',
|
|
13672
|
+
]) {
|
|
13673
|
+
delete baseDefinition[key];
|
|
13674
|
+
}
|
|
13675
|
+
const code =
|
|
13676
|
+
functionItem.code || functionItem.functionCode || functionItem.resourceCode;
|
|
13677
|
+
return stripUndefinedValues({
|
|
13678
|
+
name: functionItem.name || definitionJson.name || code,
|
|
13679
|
+
description:
|
|
13680
|
+
functionItem.description !== undefined
|
|
13681
|
+
? functionItem.description
|
|
13682
|
+
: definitionJson.description || '',
|
|
13683
|
+
definitionJson: baseDefinition,
|
|
13684
|
+
resourceBindings: clonePlainJson(definitionJson.resourceBindings || {}),
|
|
13685
|
+
inputSchema: clonePlainJson(
|
|
13686
|
+
functionItem.inputSchema || definitionJson.inputSchema
|
|
13687
|
+
),
|
|
13688
|
+
outputSchema: clonePlainJson(
|
|
13689
|
+
functionItem.outputSchema || definitionJson.outputSchema
|
|
13690
|
+
),
|
|
13691
|
+
status: functionItem.status || 'active',
|
|
13692
|
+
});
|
|
13693
|
+
}
|
|
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
|
+
|
|
11855
13764
|
function indexCurrentAutomationStates(items, codes) {
|
|
11856
13765
|
const codeSet = new Set(codes);
|
|
11857
13766
|
const groups = new Map();
|
|
@@ -11938,6 +13847,9 @@ async function applyBackendAutomationStateTransitions(
|
|
|
11938
13847
|
if (!current?.id) {
|
|
11939
13848
|
fail(`Backend Release 激活后缺少 Automation:${candidate.code} 当前版本`);
|
|
11940
13849
|
}
|
|
13850
|
+
if (candidate.mode === 'manifest_replacement') {
|
|
13851
|
+
return { code: candidate.code, transitions: [] };
|
|
13852
|
+
}
|
|
11941
13853
|
const plan = getPlannedResourceAction(
|
|
11942
13854
|
options,
|
|
11943
13855
|
'automation',
|
|
@@ -12051,14 +13963,44 @@ function saveBackendReleaseResourceStates(
|
|
|
12051
13963
|
target.bound = nextBound;
|
|
12052
13964
|
}
|
|
12053
13965
|
|
|
12054
|
-
function backendReleaseResultSummary(
|
|
13966
|
+
function backendReleaseResultSummary(
|
|
13967
|
+
release,
|
|
13968
|
+
stateTransitions = new Map(),
|
|
13969
|
+
protocolVersion = null,
|
|
13970
|
+
handledSelectors = []
|
|
13971
|
+
) {
|
|
12055
13972
|
const resources = release.resources || [];
|
|
13973
|
+
const resolvedProtocolVersion =
|
|
13974
|
+
release.protocolVersion || protocolVersion || undefined;
|
|
13975
|
+
const releaseResource = {
|
|
13976
|
+
kind: 'BackendRelease',
|
|
13977
|
+
identity: { releaseId: release.id },
|
|
13978
|
+
action: 'update',
|
|
13979
|
+
hash: release.verificationHash || release.manifestHash,
|
|
13980
|
+
metadata: resolvedProtocolVersion
|
|
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
|
+
},
|
|
13992
|
+
};
|
|
13993
|
+
const isStaged = ['prepared', 'verified'].includes(
|
|
13994
|
+
String(release.status || '').toLowerCase()
|
|
13995
|
+
);
|
|
12056
13996
|
return {
|
|
12057
13997
|
id: release.id,
|
|
12058
13998
|
status: release.status,
|
|
12059
13999
|
manifestHash: release.manifestHash,
|
|
12060
14000
|
verificationHash: release.verificationHash,
|
|
12061
14001
|
parentReleaseId: release.parentReleaseId || null,
|
|
14002
|
+
mode: release.mode,
|
|
14003
|
+
handledSelectors,
|
|
12062
14004
|
resourceCount: resources.length,
|
|
12063
14005
|
updateCount: resources.filter(resource => resource.action === 'update').length,
|
|
12064
14006
|
noopCount: resources.filter(resource => resource.action === 'noop').length,
|
|
@@ -12068,6 +14010,12 @@ function backendReleaseResultSummary(release, stateTransitions = new Map()) {
|
|
|
12068
14010
|
),
|
|
12069
14011
|
postCommitStatus: release.postCommitStatus,
|
|
12070
14012
|
postCommit: release.postCommit,
|
|
14013
|
+
...(resolvedProtocolVersion
|
|
14014
|
+
? { protocolVersion: resolvedProtocolVersion }
|
|
14015
|
+
: {}),
|
|
14016
|
+
...(isStaged
|
|
14017
|
+
? { stagedResource: releaseResource }
|
|
14018
|
+
: { activeResource: releaseResource }),
|
|
12071
14019
|
};
|
|
12072
14020
|
}
|
|
12073
14021
|
|
|
@@ -12079,7 +14027,26 @@ async function publishBackendReleaseResources(
|
|
|
12079
14027
|
options = {}
|
|
12080
14028
|
) {
|
|
12081
14029
|
const candidates = collectBackendReleaseCandidates(manifest, options);
|
|
12082
|
-
if (candidates.length === 0)
|
|
14030
|
+
if (candidates.length === 0) {
|
|
14031
|
+
if (options.stageBackendReleaseOnly) {
|
|
14032
|
+
fail('--stage-only 没有匹配到可冻结的 Function/Automation 资源');
|
|
14033
|
+
}
|
|
14034
|
+
return;
|
|
14035
|
+
}
|
|
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();
|
|
12083
14050
|
|
|
12084
14051
|
let head;
|
|
12085
14052
|
try {
|
|
@@ -12089,12 +14056,26 @@ async function publishBackendReleaseResources(
|
|
|
12089
14056
|
backendReleaseApiPath(target, '/head')
|
|
12090
14057
|
);
|
|
12091
14058
|
} catch (error) {
|
|
12092
|
-
if (
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
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
|
+
}
|
|
14072
|
+
const missing = new Error(
|
|
14073
|
+
'BACKEND_RELEASE_V2_REQUIRED: 本次 Function/Automation 发布包含 v2 operation/mode,平台必须支持 Backend Release v2,禁止降级为逐项写入'
|
|
14074
|
+
);
|
|
14075
|
+
missing.code = 'BACKEND_RELEASE_V2_REQUIRED';
|
|
14076
|
+
throw missing;
|
|
14077
|
+
}
|
|
14078
|
+
throw error;
|
|
12098
14079
|
}
|
|
12099
14080
|
|
|
12100
14081
|
const releaseContext = await ensureBackendReleasePublishContext(
|
|
@@ -12126,8 +14107,12 @@ async function publishBackendReleaseResources(
|
|
|
12126
14107
|
method: 'POST',
|
|
12127
14108
|
body: {
|
|
12128
14109
|
...releaseContext.control,
|
|
14110
|
+
...(protocolVersion ? { protocolVersion } : {}),
|
|
12129
14111
|
parentReleaseId: head?.activeBackendReleaseId || null,
|
|
12130
|
-
mode:
|
|
14112
|
+
mode: releaseMode,
|
|
14113
|
+
...(releaseMode !== 'source_only'
|
|
14114
|
+
? { replacementReason }
|
|
14115
|
+
: {}),
|
|
12131
14116
|
sourceRepositoryId: releaseContext.sourceRepositoryId,
|
|
12132
14117
|
sourceRevision: releaseContext.sourceRevision,
|
|
12133
14118
|
resources,
|
|
@@ -12157,10 +14142,55 @@ async function publishBackendReleaseResources(
|
|
|
12157
14142
|
method: 'POST',
|
|
12158
14143
|
body: {
|
|
12159
14144
|
...releaseContext.control,
|
|
14145
|
+
...(protocolVersion ? { protocolVersion } : {}),
|
|
12160
14146
|
manifestHash: prepared.manifestHash,
|
|
12161
14147
|
},
|
|
12162
14148
|
}
|
|
12163
14149
|
);
|
|
14150
|
+
if (options.stageBackendReleaseOnly) {
|
|
14151
|
+
const stagedRelease = {
|
|
14152
|
+
...prepared,
|
|
14153
|
+
...verified,
|
|
14154
|
+
resources:
|
|
14155
|
+
Array.isArray(verified.resources) && verified.resources.length > 0
|
|
14156
|
+
? verified.resources
|
|
14157
|
+
: prepared.resources || [],
|
|
14158
|
+
};
|
|
14159
|
+
if (String(stagedRelease.status || '').toLowerCase() !== 'verified') {
|
|
14160
|
+
fail('Backend Release stage-only 未获得 verified 状态,拒绝生成 stagedResource');
|
|
14161
|
+
}
|
|
14162
|
+
const handledKeys = new Set(
|
|
14163
|
+
candidates.map(candidate => `${candidate.kind}:${candidate.code}`)
|
|
14164
|
+
);
|
|
14165
|
+
options.backendReleaseSourceHandledKeys = handledKeys;
|
|
14166
|
+
options.backendReleaseHandledKeys = handledKeys;
|
|
14167
|
+
for (const resource of stagedRelease.resources) {
|
|
14168
|
+
result.published.push({
|
|
14169
|
+
kind: resource.kind === 'Function' ? 'function' : 'automation',
|
|
14170
|
+
code: resource.code,
|
|
14171
|
+
action: resource.action === 'noop' ? 'noop' : 'stage',
|
|
14172
|
+
writeMode: 'backend-release-stage',
|
|
14173
|
+
backendReleaseId: stagedRelease.id,
|
|
14174
|
+
activation: 'staged',
|
|
14175
|
+
id:
|
|
14176
|
+
resource.kind === 'Function'
|
|
14177
|
+
? resource.desired?.id || resource.resourceId
|
|
14178
|
+
: resource.desired?.id || resource.resourceId,
|
|
14179
|
+
});
|
|
14180
|
+
}
|
|
14181
|
+
result.backendRelease = backendReleaseResultSummary(
|
|
14182
|
+
stagedRelease,
|
|
14183
|
+
new Map(),
|
|
14184
|
+
protocolVersion,
|
|
14185
|
+
handledSelectors
|
|
14186
|
+
);
|
|
14187
|
+
if (!result.backendRelease.stagedResource) {
|
|
14188
|
+
fail('Backend Release stage-only 响应缺少 verified stagedResource');
|
|
14189
|
+
}
|
|
14190
|
+
result.stagedResource = result.backendRelease.stagedResource;
|
|
14191
|
+
result.handledSelectors = handledSelectors;
|
|
14192
|
+
return;
|
|
14193
|
+
}
|
|
12164
14194
|
const activated = await requestWithAuth(
|
|
12165
14195
|
config,
|
|
12166
14196
|
target.profileName,
|
|
@@ -12168,7 +14198,13 @@ async function publishBackendReleaseResources(
|
|
|
12168
14198
|
target,
|
|
12169
14199
|
`/${encodeURIComponent(verified.id || prepared.id)}/activate`
|
|
12170
14200
|
),
|
|
12171
|
-
{
|
|
14201
|
+
{
|
|
14202
|
+
method: 'POST',
|
|
14203
|
+
body: {
|
|
14204
|
+
...releaseContext.control,
|
|
14205
|
+
...(protocolVersion ? { protocolVersion } : {}),
|
|
14206
|
+
},
|
|
14207
|
+
}
|
|
12172
14208
|
);
|
|
12173
14209
|
options.backendReleaseSourceHandledKeys = new Set(
|
|
12174
14210
|
candidates.map(candidate => `${candidate.kind}:${candidate.code}`)
|
|
@@ -12232,8 +14268,11 @@ async function publishBackendReleaseResources(
|
|
|
12232
14268
|
}
|
|
12233
14269
|
result.backendRelease = backendReleaseResultSummary(
|
|
12234
14270
|
activated,
|
|
12235
|
-
stateTransitions
|
|
14271
|
+
stateTransitions,
|
|
14272
|
+
protocolVersion,
|
|
14273
|
+
handledSelectors
|
|
12236
14274
|
);
|
|
14275
|
+
result.handledSelectors = handledSelectors;
|
|
12237
14276
|
}
|
|
12238
14277
|
|
|
12239
14278
|
async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
@@ -12683,9 +14722,15 @@ async function runResourceChangeBaselinePreflight(
|
|
|
12683
14722
|
const artifactCache = { ...(baseline.localBaseArtifactHashes || {}) };
|
|
12684
14723
|
const missingArtifactTargets = targets.filter(targetItem => {
|
|
12685
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);
|
|
12686
14731
|
return (
|
|
12687
14732
|
frozen?.fields?.sourceArtifactHash &&
|
|
12688
|
-
!
|
|
14733
|
+
(!cached || (hasStableLineage && !cached.sourceHash))
|
|
12689
14734
|
);
|
|
12690
14735
|
});
|
|
12691
14736
|
if (missingArtifactTargets.length > 0) {
|
|
@@ -12716,7 +14761,26 @@ async function runResourceChangeBaselinePreflight(
|
|
|
12716
14761
|
const artifact = artifactCache[
|
|
12717
14762
|
resourceBaselineTargetKey(targetItem.kind, targetItem.code)
|
|
12718
14763
|
];
|
|
12719
|
-
|
|
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) {
|
|
12720
14784
|
fields.sourceArtifactHash =
|
|
12721
14785
|
targetItem.kind === 'Automation'
|
|
12722
14786
|
? canonicalJsonSha256([
|
|
@@ -13218,10 +15282,20 @@ function addPlanActions(actions, kind, desiredItems = [], existingMap, equals) {
|
|
|
13218
15282
|
|
|
13219
15283
|
function sourceAnalysisPlanFields(item) {
|
|
13220
15284
|
const analysis = item?.__sourceAnalysis;
|
|
13221
|
-
|
|
15285
|
+
const definition = item?.definitionJson || {};
|
|
13222
15286
|
return {
|
|
13223
|
-
|
|
13224
|
-
|
|
15287
|
+
...(analysis
|
|
15288
|
+
? {
|
|
15289
|
+
sourceDependencies: analysis.sourceDependencies,
|
|
15290
|
+
resourceReferences: analysis.resourceReferences,
|
|
15291
|
+
}
|
|
15292
|
+
: {}),
|
|
15293
|
+
...(definition.responseCache !== undefined
|
|
15294
|
+
? { responseCache: clonePlainJson(definition.responseCache) }
|
|
15295
|
+
: {}),
|
|
15296
|
+
...(definition.invocationAudit !== undefined
|
|
15297
|
+
? { invocationAudit: clonePlainJson(definition.invocationAudit) }
|
|
15298
|
+
: {}),
|
|
13225
15299
|
};
|
|
13226
15300
|
}
|
|
13227
15301
|
|
|
@@ -13592,6 +15666,54 @@ async function publishFormSettingsResources(
|
|
|
13592
15666
|
{ frozen, artifactId }
|
|
13593
15667
|
);
|
|
13594
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
|
+
}
|
|
13595
15717
|
result.published.push({
|
|
13596
15718
|
kind: 'formSetting',
|
|
13597
15719
|
code,
|
|
@@ -13602,7 +15724,9 @@ async function publishFormSettingsResources(
|
|
|
13602
15724
|
staged: Boolean(data?.staged),
|
|
13603
15725
|
active: Boolean(data?.active),
|
|
13604
15726
|
releaseStatus: data?.releaseStatus,
|
|
13605
|
-
releaseId
|
|
15727
|
+
releaseId,
|
|
15728
|
+
contentHash: contentHash || undefined,
|
|
15729
|
+
...(stagedResource ? { stagedResource } : {}),
|
|
13606
15730
|
});
|
|
13607
15731
|
}
|
|
13608
15732
|
}
|
|
@@ -14117,6 +16241,13 @@ async function publishFunctionResources(config, target, functions, result, optio
|
|
|
14117
16241
|
) {
|
|
14118
16242
|
continue;
|
|
14119
16243
|
}
|
|
16244
|
+
if (functionManifestDeclaresSecretRefs(functionItem)) {
|
|
16245
|
+
const error = new Error(
|
|
16246
|
+
`APP_FUNCTION_SECRET_STAGED_RELEASE_REQUIRED: Function:${code} secretRefs 必须由 Backend Release v2 原子发布,禁止 direct/source PATCH 降级`
|
|
16247
|
+
);
|
|
16248
|
+
error.code = 'APP_FUNCTION_SECRET_STAGED_RELEASE_REQUIRED';
|
|
16249
|
+
throw error;
|
|
16250
|
+
}
|
|
14120
16251
|
if (shouldSkipNoopResource(options, 'function', code)) {
|
|
14121
16252
|
recordNoopResource(result, 'function', code, options);
|
|
14122
16253
|
continue;
|
|
@@ -15707,6 +17838,9 @@ async function pullResources(config, target) {
|
|
|
15707
17838
|
code,
|
|
15708
17839
|
name: detail.name,
|
|
15709
17840
|
description: detail.description || '',
|
|
17841
|
+
...(Array.isArray(detail.secretRefs)
|
|
17842
|
+
? { secretRefs: normalizeFunctionSecretRefs(detail.secretRefs) }
|
|
17843
|
+
: {}),
|
|
15710
17844
|
definitionJson: detail.definitionJson,
|
|
15711
17845
|
resourceBindings: detail.resourceBindings || undefined,
|
|
15712
17846
|
inputSchema: detail.inputSchema || undefined,
|
|
@@ -17262,6 +19396,15 @@ function functionEquals(target, desired, existing) {
|
|
|
17262
19396
|
existing.resourceBindingsJson ??
|
|
17263
19397
|
existing.definitionJson?.resourceBindings ??
|
|
17264
19398
|
{};
|
|
19399
|
+
const secretRefsEqual = functionManifestDeclaresSecretRefs(desired)
|
|
19400
|
+
? jsonEqualsForPlan(
|
|
19401
|
+
normalizeFunctionSecretRefs(
|
|
19402
|
+
existing.secretRefs ?? existing.secretReferences ?? []
|
|
19403
|
+
),
|
|
19404
|
+
normalizeFunctionSecretRefs(desired.secretRefs),
|
|
19405
|
+
desired.__dir
|
|
19406
|
+
)
|
|
19407
|
+
: true;
|
|
17265
19408
|
return (
|
|
17266
19409
|
String(existing.code || existing.functionCode || existing.resourceCode || '') === String(desired.code || desired.functionCode || desired.resourceCode || '') &&
|
|
17267
19410
|
String(existing.name || '') === String(desired.name || desiredDefinition.name || desired.code || '') &&
|
|
@@ -17270,10 +19413,94 @@ function functionEquals(target, desired, existing) {
|
|
|
17270
19413
|
) &&
|
|
17271
19414
|
jsonEqualsForPlan(existing.definitionJson, desiredDefinition, desired.__dir) &&
|
|
17272
19415
|
jsonEqualsForPlan(existingResourceBindings, desiredDefinition.resourceBindings || {}, desired.__dir) &&
|
|
17273
|
-
String(existing.status || 'active') === desiredStatus
|
|
19416
|
+
String(existing.status || 'active') === desiredStatus &&
|
|
19417
|
+
secretRefsEqual
|
|
19418
|
+
);
|
|
19419
|
+
}
|
|
19420
|
+
|
|
19421
|
+
function functionManifestDeclaresSecretRefs(functionItem) {
|
|
19422
|
+
return Boolean(
|
|
19423
|
+
functionItem &&
|
|
19424
|
+
Object.prototype.hasOwnProperty.call(functionItem, 'secretRefs')
|
|
17274
19425
|
);
|
|
17275
19426
|
}
|
|
17276
19427
|
|
|
19428
|
+
function normalizeFunctionSecretRefs(value) {
|
|
19429
|
+
if (!Array.isArray(value)) return [];
|
|
19430
|
+
return value
|
|
19431
|
+
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
|
19432
|
+
.map(item => ({
|
|
19433
|
+
name: String(item.name || '').trim(),
|
|
19434
|
+
required: item.required !== false,
|
|
19435
|
+
}))
|
|
19436
|
+
.filter(item => item.name)
|
|
19437
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
19438
|
+
}
|
|
19439
|
+
|
|
19440
|
+
function validateFunctionSecretRefs(label, item, errors) {
|
|
19441
|
+
if (!functionManifestDeclaresSecretRefs(item)) return;
|
|
19442
|
+
if (!Array.isArray(item.secretRefs)) {
|
|
19443
|
+
errors.push(`${label}: secretRefs 必须是数组`);
|
|
19444
|
+
return;
|
|
19445
|
+
}
|
|
19446
|
+
const names = new Set();
|
|
19447
|
+
for (const [index, ref] of item.secretRefs.entries()) {
|
|
19448
|
+
const refLabel = `${label}.secretRefs[${index}]`;
|
|
19449
|
+
if (!ref || typeof ref !== 'object' || Array.isArray(ref)) {
|
|
19450
|
+
errors.push(`${refLabel}: 必须是 {name, required} 对象`);
|
|
19451
|
+
continue;
|
|
19452
|
+
}
|
|
19453
|
+
const extraKeys = Object.keys(ref).filter(
|
|
19454
|
+
key => !['name', 'required'].includes(key)
|
|
19455
|
+
);
|
|
19456
|
+
if (extraKeys.length > 0) {
|
|
19457
|
+
errors.push(
|
|
19458
|
+
`${refLabel}: 仅允许 name/required metadata,禁止 value/file/env 等字段: ${extraKeys.join(', ')}`
|
|
19459
|
+
);
|
|
19460
|
+
}
|
|
19461
|
+
const name = String(ref.name || '').trim();
|
|
19462
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(name)) {
|
|
19463
|
+
errors.push(`${refLabel}.name: 格式无效`);
|
|
19464
|
+
} else if (names.has(name)) {
|
|
19465
|
+
errors.push(`${refLabel}.name: 重复声明 ${name}`);
|
|
19466
|
+
} else {
|
|
19467
|
+
names.add(name);
|
|
19468
|
+
}
|
|
19469
|
+
if (typeof ref.required !== 'boolean') {
|
|
19470
|
+
errors.push(`${refLabel}.required: 必须显式为 boolean`);
|
|
19471
|
+
}
|
|
19472
|
+
}
|
|
19473
|
+
let definition = item.definitionJson;
|
|
19474
|
+
if (!definition && item.definitionFile) {
|
|
19475
|
+
try {
|
|
19476
|
+
definition = resolveManifestPlainJson(
|
|
19477
|
+
item,
|
|
19478
|
+
'definitionJson',
|
|
19479
|
+
'definitionFile'
|
|
19480
|
+
);
|
|
19481
|
+
} catch (error) {
|
|
19482
|
+
errors.push(`${label}: 无法校验 secretRefs runtime contract: ${error.message}`);
|
|
19483
|
+
return;
|
|
19484
|
+
}
|
|
19485
|
+
}
|
|
19486
|
+
if (definition?.secretRefs !== undefined) {
|
|
19487
|
+
errors.push(`${label}: secretRefs 必须位于 Function resource 顶层,不能进入 definitionJson/source snapshot`);
|
|
19488
|
+
}
|
|
19489
|
+
if (String(definition?.version || '') !== 'function_v2') {
|
|
19490
|
+
errors.push(`${label}: 声明 secretRefs 时 definitionJson.version 必须是 function_v2`);
|
|
19491
|
+
}
|
|
19492
|
+
if (String(definition?.runtimeMode || '') !== 'trusted_node') {
|
|
19493
|
+
errors.push(`${label}: 声明 secretRefs 时 runtimeMode 必须是 trusted_node`);
|
|
19494
|
+
}
|
|
19495
|
+
if (
|
|
19496
|
+
String(definition?.runtimeContractVersion || '') !== 'trusted_node_v2'
|
|
19497
|
+
) {
|
|
19498
|
+
errors.push(
|
|
19499
|
+
`${label}: 声明 secretRefs 时 runtimeContractVersion 必须是 trusted_node_v2`
|
|
19500
|
+
);
|
|
19501
|
+
}
|
|
19502
|
+
}
|
|
19503
|
+
|
|
17277
19504
|
function normalizeFunctionDefinitionJson(functionItem, definitionJson) {
|
|
17278
19505
|
const definition = clonePlainJson(definitionJson || {});
|
|
17279
19506
|
const functionCode =
|
|
@@ -17472,11 +19699,37 @@ function normalizeJsonForPlan(value, baseDir, keyName) {
|
|
|
17472
19699
|
result.code = result.code === undefined ? '' : result.code;
|
|
17473
19700
|
result.runtimeMode = result.runtimeMode || 'trusted_node';
|
|
17474
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
|
+
}
|
|
17475
19712
|
}
|
|
17476
19713
|
if (result.code === '') delete result.code;
|
|
17477
19714
|
return sortObjectForStableJson(result);
|
|
17478
19715
|
}
|
|
17479
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
|
+
|
|
17480
19733
|
function normalizeLocalSourceFileForPlan(localPath, baseDir) {
|
|
17481
19734
|
const rawLocalPath = String(localPath);
|
|
17482
19735
|
const baseResolvedPath = path.resolve(baseDir || process.cwd(), rawLocalPath);
|
|
@@ -17698,7 +19951,7 @@ async function resolveJsCodeSnapshotFiles(config, profileName, value, baseDir, u
|
|
|
17698
19951
|
typeof value.sourceFile === 'object' &&
|
|
17699
19952
|
value.sourceFile.localPath
|
|
17700
19953
|
) {
|
|
17701
|
-
const
|
|
19954
|
+
const snapshot = await uploadJsCodeSnapshotFile(
|
|
17702
19955
|
config,
|
|
17703
19956
|
profileName,
|
|
17704
19957
|
value.sourceFile.localPath,
|
|
@@ -17708,7 +19961,13 @@ async function resolveJsCodeSnapshotFiles(config, profileName, value, baseDir, u
|
|
|
17708
19961
|
);
|
|
17709
19962
|
value.runtimeMode = value.runtimeMode || 'trusted_node';
|
|
17710
19963
|
value.sourceType = 'file_snapshot';
|
|
17711
|
-
value.sourceFile = sourceFile;
|
|
19964
|
+
value.sourceFile = snapshot.sourceFile;
|
|
19965
|
+
if (snapshot.sourceProvenance) {
|
|
19966
|
+
value.sourceProvenance = {
|
|
19967
|
+
...(value.sourceProvenance || {}),
|
|
19968
|
+
...snapshot.sourceProvenance,
|
|
19969
|
+
};
|
|
19970
|
+
}
|
|
17712
19971
|
value.code = value.code || '';
|
|
17713
19972
|
}
|
|
17714
19973
|
|
|
@@ -17743,6 +20002,14 @@ async function uploadJsCodeSnapshotFile(
|
|
|
17743
20002
|
if (uploadCache.has(cacheKey)) return await uploadCache.get(cacheKey);
|
|
17744
20003
|
|
|
17745
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;
|
|
17746
20013
|
const pending = requestFormWithAuth(
|
|
17747
20014
|
config,
|
|
17748
20015
|
profileName,
|
|
@@ -17762,10 +20029,14 @@ async function uploadJsCodeSnapshotFile(
|
|
|
17762
20029
|
originalName: uploaded.originalName,
|
|
17763
20030
|
contentType: uploaded.contentType,
|
|
17764
20031
|
};
|
|
17765
|
-
|
|
20032
|
+
const snapshot = {
|
|
20033
|
+
sourceFile,
|
|
20034
|
+
...(sourceProvenance ? { sourceProvenance } : {}),
|
|
20035
|
+
};
|
|
20036
|
+
uploadCache.set(cacheKey, snapshot);
|
|
17766
20037
|
// Progress belongs on stderr so `--json` keeps stdout machine-readable.
|
|
17767
20038
|
warn(`已上传JS_CODE快照: ${path.relative(process.cwd(), bundlePath)} (${sourceFile.sha256})`);
|
|
17768
|
-
return
|
|
20039
|
+
return snapshot;
|
|
17769
20040
|
});
|
|
17770
20041
|
uploadCache.set(cacheKey, pending);
|
|
17771
20042
|
try {
|
|
@@ -17776,6 +20047,38 @@ async function uploadJsCodeSnapshotFile(
|
|
|
17776
20047
|
}
|
|
17777
20048
|
}
|
|
17778
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
|
+
|
|
17779
20082
|
async function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
17780
20083
|
const extension = path.extname(localPath).toLowerCase();
|
|
17781
20084
|
if (['.js', '.cjs', '.mjs'].includes(extension)) {
|
|
@@ -17837,10 +20140,10 @@ async function resolveJsCodeBundlePath(localPath, scriptCode) {
|
|
|
17837
20140
|
}
|
|
17838
20141
|
|
|
17839
20142
|
function runWorkspaceJsCodeBuild(workspaceRoot, resolvedScriptCode, sourceKind) {
|
|
17840
|
-
return
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
20143
|
+
return runWorkspaceJsCodeBuildBatch(
|
|
20144
|
+
workspaceRoot,
|
|
20145
|
+
[{ sourceKind, scriptCode: resolvedScriptCode }]
|
|
20146
|
+
);
|
|
17844
20147
|
}
|
|
17845
20148
|
|
|
17846
20149
|
function isUnsupportedFunctionsBuildScript(result) {
|
|
@@ -18154,26 +20457,7 @@ function getChangedManifestResourceCodes(file, resourceKey, baseRef = 'HEAD') {
|
|
|
18154
20457
|
}
|
|
18155
20458
|
|
|
18156
20459
|
function shouldReplaceResourceManifestItem(item, resourceKey, code, options = {}) {
|
|
18157
|
-
|
|
18158
|
-
if (!item?.__source) return false;
|
|
18159
|
-
const relativeFile = normalizeWorkspaceRelativePath(
|
|
18160
|
-
path.relative(process.cwd(), path.resolve(item.__source))
|
|
18161
|
-
);
|
|
18162
|
-
const scopedFiles = new Set(
|
|
18163
|
-
(options.scopedFiles || []).map(normalizeWorkspaceRelativePath)
|
|
18164
|
-
);
|
|
18165
|
-
if (!scopedFiles.has(relativeFile)) return false;
|
|
18166
|
-
const changedCodes = getChangedManifestResourceCodes(
|
|
18167
|
-
relativeFile,
|
|
18168
|
-
resourceKey,
|
|
18169
|
-
options.baseRef || 'HEAD'
|
|
18170
|
-
);
|
|
18171
|
-
if (changedCodes.length === 0) {
|
|
18172
|
-
// The SDD/change scope may refer to a committed manifest change that no
|
|
18173
|
-
// longer differs from HEAD inside the release worktree.
|
|
18174
|
-
return true;
|
|
18175
|
-
}
|
|
18176
|
-
return changedCodes.includes(String(code));
|
|
20460
|
+
return options.replaceManifest === true;
|
|
18177
20461
|
}
|
|
18178
20462
|
|
|
18179
20463
|
function buildAutomationSourcePatchSources(existingDefinition, desiredDefinition) {
|
|
@@ -18527,19 +20811,27 @@ function scanWorkspaceReliabilityIssues(files) {
|
|
|
18527
20811
|
|
|
18528
20812
|
function buildTargetedResourceReleaseCommands(targets, profileArg, changeId) {
|
|
18529
20813
|
const changeArg = changeId ? ` --change ${changeId}` : '';
|
|
18530
|
-
if (targets.resources || (targets.jsCodeNodes || []).length > 0) {
|
|
18531
|
-
return [`openxiangda resource publish --profile ${profileArg}${changeArg}`];
|
|
18532
|
-
}
|
|
18533
20814
|
const commands = [];
|
|
18534
|
-
|
|
18535
|
-
|
|
18536
|
-
|
|
18537
|
-
[
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
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) {
|
|
18541
20833
|
commands.push(
|
|
18542
|
-
`openxiangda resource publish
|
|
20834
|
+
`openxiangda resource publish workflow --only ${workflows.join(',')} --profile ${profileArg}${changeArg}`
|
|
18543
20835
|
);
|
|
18544
20836
|
}
|
|
18545
20837
|
return commands;
|
|
@@ -18555,9 +20847,9 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile, changeId)
|
|
|
18555
20847
|
(targets.workflows || []).length > 0 ||
|
|
18556
20848
|
(targets.jsCodeNodes || []).length > 0;
|
|
18557
20849
|
if (runtimeMode === 'react-spa') {
|
|
18558
|
-
|
|
20850
|
+
if (targets.forms.length > 0) {
|
|
18559
20851
|
commands.push(
|
|
18560
|
-
`openxiangda
|
|
20852
|
+
`openxiangda resource publish form-setting --only ${targets.forms.join(',')} --profile ${profileArg}${changeId ? ` --change ${changeId}` : ''}`
|
|
18561
20853
|
);
|
|
18562
20854
|
}
|
|
18563
20855
|
if (hasResourceTargets) {
|
|
@@ -18565,7 +20857,17 @@ function buildWorkspaceReleaseCommands(targets, runtimeMode, profile, changeId)
|
|
|
18565
20857
|
}
|
|
18566
20858
|
if (targets.runtime) {
|
|
18567
20859
|
commands.push(
|
|
18568
|
-
`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}` : ''}`
|
|
18569
20871
|
);
|
|
18570
20872
|
}
|
|
18571
20873
|
return commands;
|
|
@@ -18629,12 +20931,35 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
18629
20931
|
name: issue.rule,
|
|
18630
20932
|
message: `${issue.file}:${issue.line} ${issue.message}`,
|
|
18631
20933
|
}));
|
|
18632
|
-
const
|
|
20934
|
+
const generatedCommands = buildWorkspaceReleaseCommands(
|
|
18633
20935
|
targets,
|
|
18634
20936
|
runtimeMode,
|
|
18635
20937
|
options.profile,
|
|
18636
20938
|
changeScope?.changeId
|
|
18637
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
|
+
}
|
|
18638
20963
|
const sdd = options.skipSdd
|
|
18639
20964
|
? null
|
|
18640
20965
|
: buildSddGovernanceReport({
|
|
@@ -18749,6 +21074,7 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
|
|
|
18749
21074
|
commands,
|
|
18750
21075
|
changeId: options.changeId,
|
|
18751
21076
|
verifyStage: options.verifyStage || 'prepublish',
|
|
21077
|
+
actualCommands: options.actualCommands === true,
|
|
18752
21078
|
});
|
|
18753
21079
|
return {
|
|
18754
21080
|
targets,
|
|
@@ -18760,6 +21086,21 @@ function buildExplicitSddReleasePlan(targets, options = {}) {
|
|
|
18760
21086
|
|
|
18761
21087
|
function assertSddReleaseGate(plan, flags, command) {
|
|
18762
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
|
+
}
|
|
18763
21104
|
if (flags['sdd-bypass']) {
|
|
18764
21105
|
const bypass = appendBypassLog({
|
|
18765
21106
|
cwd: process.cwd(),
|