openxiangda 1.0.178 → 1.0.180
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 +18 -1
- package/lib/application-environments.js +282 -0
- package/lib/cli.js +1122 -49
- package/lib/developer-center.js +157 -0
- package/lib/policy.js +16 -0
- package/openxiangda-skills/SKILL.md +13 -2
- package/openxiangda-skills/references/openxiangda-api.md +40 -0
- package/openxiangda-skills/references/workspace-state.md +37 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +12 -1
- package/package.json +2 -1
- package/policy/openxiangda-policy.json +7 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +2 -1
- package/templates/openxiangda-react-spa/AGENTS.md +14 -2
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +3 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +2 -0
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +2 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +4 -2
package/lib/cli.js
CHANGED
|
@@ -67,6 +67,7 @@ const {
|
|
|
67
67
|
const {
|
|
68
68
|
buildWorkspaceReleaseCommands,
|
|
69
69
|
buildWorkspaceReleaseSteps,
|
|
70
|
+
commandFromArgs,
|
|
70
71
|
normalizeReleaseTargets,
|
|
71
72
|
releasePlanHash,
|
|
72
73
|
} = require('./release-plan');
|
|
@@ -126,6 +127,19 @@ const {
|
|
|
126
127
|
renderResourceExplain,
|
|
127
128
|
} = require('./design-gates');
|
|
128
129
|
const { buildDesignReview, renderDesignReview } = require('./design-review');
|
|
130
|
+
const {
|
|
131
|
+
bindEnvironmentTarget,
|
|
132
|
+
buildCandidateBundle,
|
|
133
|
+
normalizeEnvironmentKind,
|
|
134
|
+
readCandidate,
|
|
135
|
+
readJsonInput,
|
|
136
|
+
releaseExecutionPath,
|
|
137
|
+
resolveManagedStateBinding,
|
|
138
|
+
resolveEnvironmentTarget,
|
|
139
|
+
saveCandidate,
|
|
140
|
+
sha256Canonical,
|
|
141
|
+
} = require('./application-environments');
|
|
142
|
+
const { startDeveloperCenter } = require('./developer-center');
|
|
129
143
|
const {
|
|
130
144
|
describeOpenApiOperation,
|
|
131
145
|
listOpenApiOperations,
|
|
@@ -229,7 +243,9 @@ async function mainImpl(argv) {
|
|
|
229
243
|
if (command === 'open-api') return openApi(rest);
|
|
230
244
|
if (command === 'design') return design(rest);
|
|
231
245
|
if (command === 'sdd') return sdd(rest);
|
|
246
|
+
if (command === 'environment') return environment(rest);
|
|
232
247
|
if (command === 'release') return release(rest);
|
|
248
|
+
if (command === 'studio') return studio(rest);
|
|
233
249
|
if (command === 'task') return task(rest);
|
|
234
250
|
if (command === 'workspace') return workspace(rest);
|
|
235
251
|
if (command === 'app') return app(rest);
|
|
@@ -278,7 +294,9 @@ Usage:
|
|
|
278
294
|
openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
|
|
279
295
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
280
296
|
openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
297
|
+
openxiangda environment init|attach|bind|list|status|diff|use [preproduction|production] [--logical-app code] [--environment target] [--json]
|
|
281
298
|
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
|
|
299
|
+
openxiangda release candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--json]
|
|
282
300
|
openxiangda task status --change <id> [--profile name] [--watch] [--json]
|
|
283
301
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
284
302
|
openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
|
|
@@ -344,6 +362,7 @@ Usage:
|
|
|
344
362
|
openxiangda skill install [--agent codex|claude|qoder|dual] [--dest <skills-dir>] [--force] [--dry-run] [--json]
|
|
345
363
|
openxiangda skill status [--agent codex|claude|qoder|dual] [--dest <skills-dir>] [--json]
|
|
346
364
|
openxiangda skill bootstrap [<dir>] [--force] [--dry-run] [--json]
|
|
365
|
+
openxiangda studio [--no-open] [--port 0]
|
|
347
366
|
|
|
348
367
|
OpenXiangda 应用开发使用普通用户登录 token,不需要 AK/SK;外部后端集成使用 open-api skill 与 AK/SK。
|
|
349
368
|
所有直接配置写入必须携带 --change <id>,或先执行 release begin --change <id>;dry-run、schema-plan、validate、query/invoke 等只读/数据面命令不会获取发布租约。
|
|
@@ -366,6 +385,7 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
366
385
|
'--legacy-form-bundle',
|
|
367
386
|
'--no-activate',
|
|
368
387
|
'--no-build',
|
|
388
|
+
'--no-open',
|
|
369
389
|
'--no-runtime-aliases',
|
|
370
390
|
'--prune',
|
|
371
391
|
'--publish',
|
|
@@ -375,6 +395,7 @@ const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
|
|
|
375
395
|
'--resources',
|
|
376
396
|
'--skip-resources',
|
|
377
397
|
'--show-secret',
|
|
398
|
+
'--confirm-production',
|
|
378
399
|
'--stage-only',
|
|
379
400
|
'--strict',
|
|
380
401
|
'--sdd-bypass',
|
|
@@ -1031,8 +1052,8 @@ function releaseExecutionDir(changeId) {
|
|
|
1031
1052
|
);
|
|
1032
1053
|
}
|
|
1033
1054
|
|
|
1034
|
-
function releaseExecutionFile(changeId) {
|
|
1035
|
-
return
|
|
1055
|
+
function releaseExecutionFile(changeId, deploymentId) {
|
|
1056
|
+
return releaseExecutionPath(changeId, deploymentId, process.cwd());
|
|
1036
1057
|
}
|
|
1037
1058
|
|
|
1038
1059
|
function directPublishReceiptFile(changeId) {
|
|
@@ -1048,8 +1069,8 @@ function writePrivateJsonAtomic(file, value) {
|
|
|
1048
1069
|
fs.renameSync(tempFile, file);
|
|
1049
1070
|
}
|
|
1050
1071
|
|
|
1051
|
-
function readReleaseExecution(changeId) {
|
|
1052
|
-
const file = releaseExecutionFile(changeId);
|
|
1072
|
+
function readReleaseExecution(changeId, deploymentId) {
|
|
1073
|
+
const file = releaseExecutionFile(changeId, deploymentId);
|
|
1053
1074
|
if (!fs.existsSync(file)) return null;
|
|
1054
1075
|
try {
|
|
1055
1076
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -1141,6 +1162,7 @@ function releaseExecutionContextFromBegin(result, target, changeId) {
|
|
|
1141
1162
|
appType: target.appType,
|
|
1142
1163
|
profile: target.profileName,
|
|
1143
1164
|
changeId,
|
|
1165
|
+
deploymentId: target.deploymentId || null,
|
|
1144
1166
|
leaseId: String(value.leaseId),
|
|
1145
1167
|
baselineId: value.baselineId ? String(value.baselineId) : null,
|
|
1146
1168
|
clientSessionId: String(value.clientSessionId),
|
|
@@ -1406,8 +1428,42 @@ async function endReleaseWithoutLocalLease(
|
|
|
1406
1428
|
};
|
|
1407
1429
|
}
|
|
1408
1430
|
|
|
1431
|
+
function decorateEnvironmentReleaseSteps(steps, target, flags = {}) {
|
|
1432
|
+
if (!target.environmentId || !target.targetName) return steps;
|
|
1433
|
+
const deploymentId = readStringFlag(flags, 'deployment-id');
|
|
1434
|
+
for (const step of steps) {
|
|
1435
|
+
step.args.push('--environment', target.targetName);
|
|
1436
|
+
if (deploymentId) step.args.push('--deployment-id', deploymentId);
|
|
1437
|
+
if (step.id === 'app-finalize') {
|
|
1438
|
+
for (const [flag, value] of [
|
|
1439
|
+
['--environment-id', target.environmentId],
|
|
1440
|
+
['--candidate-id', readStringFlag(flags, 'candidate-id')],
|
|
1441
|
+
]) {
|
|
1442
|
+
if (value) step.args.push(flag, value);
|
|
1443
|
+
}
|
|
1444
|
+
const stagedIndex = step.args.indexOf('--staged-resources-json');
|
|
1445
|
+
if (stagedIndex >= 0 && deploymentId) {
|
|
1446
|
+
step.args[stagedIndex + 1] = path
|
|
1447
|
+
.relative(
|
|
1448
|
+
process.cwd(),
|
|
1449
|
+
stagedResourcesFileForChange(changeIdFromStep(step), deploymentId)
|
|
1450
|
+
)
|
|
1451
|
+
.replace(/\\/g, '/');
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
step.command = commandFromArgs(step.args);
|
|
1455
|
+
}
|
|
1456
|
+
return steps;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function changeIdFromStep(step) {
|
|
1460
|
+
const index = step.args.indexOf('--change');
|
|
1461
|
+
return index >= 0 ? step.args[index + 1] : 'change';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1409
1464
|
async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
1410
1465
|
const changeId = readStringFlag(flags, 'change');
|
|
1466
|
+
const deploymentId = readStringFlag(flags, 'deployment-id') || null;
|
|
1411
1467
|
if (!changeId) {
|
|
1412
1468
|
fail('用法: openxiangda release publish --change <id> --profile <name>');
|
|
1413
1469
|
}
|
|
@@ -1432,6 +1488,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1432
1488
|
target.profileName,
|
|
1433
1489
|
changeId
|
|
1434
1490
|
);
|
|
1491
|
+
decorateEnvironmentReleaseSteps(steps, target, flags);
|
|
1435
1492
|
const releaseScope = getSddChangeScope({
|
|
1436
1493
|
cwd: process.cwd(),
|
|
1437
1494
|
configText: readWorkspaceConfigText(),
|
|
@@ -1503,7 +1560,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1503
1560
|
const plannedSourceRevision = prepareReleaseSourceRevision({
|
|
1504
1561
|
cwd: process.cwd(),
|
|
1505
1562
|
});
|
|
1506
|
-
const existing = readReleaseExecution(changeId);
|
|
1563
|
+
const existing = readReleaseExecution(changeId, deploymentId);
|
|
1507
1564
|
if (
|
|
1508
1565
|
existing &&
|
|
1509
1566
|
(existing.planHash !== planHash ||
|
|
@@ -1533,6 +1590,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1533
1590
|
appType: target.appType,
|
|
1534
1591
|
profile: target.profileName,
|
|
1535
1592
|
changeId,
|
|
1593
|
+
...(deploymentId ? { deploymentId } : {}),
|
|
1536
1594
|
planHash,
|
|
1537
1595
|
releaseSourceRevision: plannedSourceRevision,
|
|
1538
1596
|
status: 'planned',
|
|
@@ -1543,7 +1601,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1543
1601
|
steps: [
|
|
1544
1602
|
{
|
|
1545
1603
|
id: 'lease-and-capture',
|
|
1546
|
-
command: `openxiangda release begin --change ${changeId} --profile ${target.profileName}
|
|
1604
|
+
command: `openxiangda release begin --change ${changeId} --profile ${target.profileName}${
|
|
1605
|
+
target.targetName ? ` --environment ${target.targetName}` : ''
|
|
1606
|
+
} --freeze-app-capture`,
|
|
1547
1607
|
status: 'pending',
|
|
1548
1608
|
},
|
|
1549
1609
|
...steps.map(step => ({
|
|
@@ -1554,7 +1614,9 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1554
1614
|
})),
|
|
1555
1615
|
{
|
|
1556
1616
|
id: 'release-lease',
|
|
1557
|
-
command: `openxiangda release end --change ${changeId} --profile ${target.profileName}
|
|
1617
|
+
command: `openxiangda release end --change ${changeId} --profile ${target.profileName}${
|
|
1618
|
+
target.targetName ? ` --environment ${target.targetName}` : ''
|
|
1619
|
+
}`,
|
|
1558
1620
|
status: 'pending',
|
|
1559
1621
|
},
|
|
1560
1622
|
],
|
|
@@ -1571,14 +1633,17 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1571
1633
|
changeId,
|
|
1572
1634
|
planHash,
|
|
1573
1635
|
executionFile: path
|
|
1574
|
-
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1636
|
+
.relative(process.cwd(), releaseExecutionFile(changeId, deploymentId))
|
|
1575
1637
|
.replace(/\\/g, '/'),
|
|
1576
1638
|
execution,
|
|
1577
1639
|
};
|
|
1578
1640
|
}
|
|
1579
1641
|
execution.status = 'running';
|
|
1580
1642
|
execution.updatedAt = new Date().toISOString();
|
|
1581
|
-
writePrivateJsonAtomic(
|
|
1643
|
+
writePrivateJsonAtomic(
|
|
1644
|
+
releaseExecutionFile(changeId, deploymentId),
|
|
1645
|
+
execution
|
|
1646
|
+
);
|
|
1582
1647
|
|
|
1583
1648
|
const runnable = [
|
|
1584
1649
|
{
|
|
@@ -1590,6 +1655,12 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1590
1655
|
changeId,
|
|
1591
1656
|
'--profile',
|
|
1592
1657
|
target.profileName,
|
|
1658
|
+
...(target.targetName
|
|
1659
|
+
? ['--environment', target.targetName]
|
|
1660
|
+
: []),
|
|
1661
|
+
...(deploymentId
|
|
1662
|
+
? ['--deployment-id', deploymentId]
|
|
1663
|
+
: []),
|
|
1593
1664
|
'--freeze-app-capture',
|
|
1594
1665
|
],
|
|
1595
1666
|
},
|
|
@@ -1603,6 +1674,12 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1603
1674
|
changeId,
|
|
1604
1675
|
'--profile',
|
|
1605
1676
|
target.profileName,
|
|
1677
|
+
...(target.targetName
|
|
1678
|
+
? ['--environment', target.targetName]
|
|
1679
|
+
: []),
|
|
1680
|
+
...(deploymentId
|
|
1681
|
+
? ['--deployment-id', deploymentId]
|
|
1682
|
+
: []),
|
|
1606
1683
|
],
|
|
1607
1684
|
},
|
|
1608
1685
|
];
|
|
@@ -1616,7 +1693,10 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1616
1693
|
journalStep.status = 'running';
|
|
1617
1694
|
journalStep.startedAt = new Date().toISOString();
|
|
1618
1695
|
execution.updatedAt = journalStep.startedAt;
|
|
1619
|
-
writePrivateJsonAtomic(
|
|
1696
|
+
writePrivateJsonAtomic(
|
|
1697
|
+
releaseExecutionFile(changeId, deploymentId),
|
|
1698
|
+
execution
|
|
1699
|
+
);
|
|
1620
1700
|
warn(`发布步骤开始: ${step.id}`);
|
|
1621
1701
|
const result = await runOpenXiangdaInProcess(step.args);
|
|
1622
1702
|
journalStep.status = 'completed';
|
|
@@ -1638,12 +1718,18 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1638
1718
|
}
|
|
1639
1719
|
if (step.stagedKind) execution.stagedWriteOccurred = true;
|
|
1640
1720
|
execution.updatedAt = journalStep.completedAt;
|
|
1641
|
-
writePrivateJsonAtomic(
|
|
1721
|
+
writePrivateJsonAtomic(
|
|
1722
|
+
releaseExecutionFile(changeId, deploymentId),
|
|
1723
|
+
execution
|
|
1724
|
+
);
|
|
1642
1725
|
}
|
|
1643
1726
|
execution.status = 'completed';
|
|
1644
1727
|
execution.completedAt = new Date().toISOString();
|
|
1645
1728
|
execution.updatedAt = execution.completedAt;
|
|
1646
|
-
writePrivateJsonAtomic(
|
|
1729
|
+
writePrivateJsonAtomic(
|
|
1730
|
+
releaseExecutionFile(changeId, deploymentId),
|
|
1731
|
+
execution
|
|
1732
|
+
);
|
|
1647
1733
|
return {
|
|
1648
1734
|
completed: true,
|
|
1649
1735
|
resumed: Boolean(existing),
|
|
@@ -1652,7 +1738,7 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1652
1738
|
changeId,
|
|
1653
1739
|
planHash,
|
|
1654
1740
|
executionFile: path
|
|
1655
|
-
.relative(process.cwd(), releaseExecutionFile(changeId))
|
|
1741
|
+
.relative(process.cwd(), releaseExecutionFile(changeId, deploymentId))
|
|
1656
1742
|
.replace(/\\/g, '/'),
|
|
1657
1743
|
execution,
|
|
1658
1744
|
};
|
|
@@ -1684,7 +1770,10 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1684
1770
|
writeAttempted: execution.writeAttempted,
|
|
1685
1771
|
});
|
|
1686
1772
|
execution.updatedAt = new Date().toISOString();
|
|
1687
|
-
writePrivateJsonAtomic(
|
|
1773
|
+
writePrivateJsonAtomic(
|
|
1774
|
+
releaseExecutionFile(changeId, deploymentId),
|
|
1775
|
+
execution
|
|
1776
|
+
);
|
|
1688
1777
|
if (!execution.writeAttempted) {
|
|
1689
1778
|
try {
|
|
1690
1779
|
await runOpenXiangdaInProcess(
|
|
@@ -1695,6 +1784,12 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1695
1784
|
changeId,
|
|
1696
1785
|
'--profile',
|
|
1697
1786
|
target.profileName,
|
|
1787
|
+
...(target.targetName
|
|
1788
|
+
? ['--environment', target.targetName]
|
|
1789
|
+
: []),
|
|
1790
|
+
...(deploymentId
|
|
1791
|
+
? ['--deployment-id', deploymentId]
|
|
1792
|
+
: []),
|
|
1698
1793
|
],
|
|
1699
1794
|
{ quiet: true }
|
|
1700
1795
|
);
|
|
@@ -1706,12 +1801,873 @@ async function publishWorkspaceRelease(config, target, flags = {}) {
|
|
|
1706
1801
|
}
|
|
1707
1802
|
}
|
|
1708
1803
|
|
|
1804
|
+
function environmentSetApiPath(code, suffix = '') {
|
|
1805
|
+
const base = '/openxiangda-api/v1/environment-sets';
|
|
1806
|
+
return code
|
|
1807
|
+
? `${base}/${encodeURIComponent(code)}${suffix}`
|
|
1808
|
+
: base;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function defaultEnvironmentSideEffectPolicy(kind) {
|
|
1812
|
+
return kind === 'production'
|
|
1813
|
+
? {
|
|
1814
|
+
notifications: 'real',
|
|
1815
|
+
organizationWrites: 'explicit_capability_only',
|
|
1816
|
+
scheduledAutomations: 'enabled',
|
|
1817
|
+
externalWrites: 'configured',
|
|
1818
|
+
payments: 'configured',
|
|
1819
|
+
publicIndexing: 'configured',
|
|
1820
|
+
environmentBanner: false,
|
|
1821
|
+
}
|
|
1822
|
+
: {
|
|
1823
|
+
notifications: 'tester_allowlist',
|
|
1824
|
+
notificationAllowlist: [],
|
|
1825
|
+
organizationWrites: 'deny',
|
|
1826
|
+
scheduledAutomations: 'disabled',
|
|
1827
|
+
externalWrites: 'allowlist',
|
|
1828
|
+
payments: 'deny',
|
|
1829
|
+
publicIndexing: 'deny',
|
|
1830
|
+
environmentBanner: true,
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function saveEnvironmentWorkspaceState(state, setDetail, environmentDetail, input) {
|
|
1835
|
+
state.logicalApp = {
|
|
1836
|
+
id: setDetail.id,
|
|
1837
|
+
code: setDetail.code,
|
|
1838
|
+
name: setDetail.name,
|
|
1839
|
+
sourceRepositoryId: setDetail.sourceRepositoryId,
|
|
1840
|
+
updatedAt: new Date().toISOString(),
|
|
1841
|
+
};
|
|
1842
|
+
const legacyBound = state.profiles?.[input.profile];
|
|
1843
|
+
const seedResources =
|
|
1844
|
+
legacyBound?.appType === environmentDetail.appType
|
|
1845
|
+
? legacyBound.resources
|
|
1846
|
+
: undefined;
|
|
1847
|
+
const bound = bindEnvironmentTarget(state, {
|
|
1848
|
+
targetName: input.targetName,
|
|
1849
|
+
profile: input.profile,
|
|
1850
|
+
environmentId: environmentDetail.id,
|
|
1851
|
+
kind: environmentDetail.kind,
|
|
1852
|
+
appType: environmentDetail.appType,
|
|
1853
|
+
displayName: environmentDetail.displayName,
|
|
1854
|
+
publicOrigin: environmentDetail.publicOrigin,
|
|
1855
|
+
sideEffectPolicy: environmentDetail.sideEffectPolicy,
|
|
1856
|
+
seedResources,
|
|
1857
|
+
});
|
|
1858
|
+
ensureResourceBuckets(bound);
|
|
1859
|
+
saveProjectState(state);
|
|
1860
|
+
return bound;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
async function environment(args) {
|
|
1864
|
+
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
1865
|
+
const { flags, positional } = parseArgs(rest);
|
|
1866
|
+
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1867
|
+
print([
|
|
1868
|
+
'用法: openxiangda environment init --logical-app <code> --name <name> --kind preproduction --app-type APP_XXX [--target name] [--profile name]',
|
|
1869
|
+
' openxiangda environment attach --logical-app <code> [--preproduction-target name] [--production-target name] [--profile name]',
|
|
1870
|
+
' openxiangda environment bind preproduction|production --app-type APP_XXX [--target name] [--profile name]',
|
|
1871
|
+
' openxiangda environment list|status|diff [--logical-app code] [--profile name] [--json]',
|
|
1872
|
+
' openxiangda environment use <target|preproduction|production>',
|
|
1873
|
+
].join('\n'));
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const config = loadConfig();
|
|
1877
|
+
const state = loadProjectState();
|
|
1878
|
+
|
|
1879
|
+
if (subcommand === 'use') {
|
|
1880
|
+
const selected = resolveEnvironmentTarget(
|
|
1881
|
+
state,
|
|
1882
|
+
positional[0] || flags.environment || flags.target
|
|
1883
|
+
);
|
|
1884
|
+
state.currentTarget = selected.targetName;
|
|
1885
|
+
saveProjectState(state);
|
|
1886
|
+
const result = {
|
|
1887
|
+
currentTarget: selected.targetName,
|
|
1888
|
+
kind: selected.binding.kind,
|
|
1889
|
+
appType: selected.binding.appType,
|
|
1890
|
+
};
|
|
1891
|
+
if (flags.json) return writeJson(result);
|
|
1892
|
+
print(
|
|
1893
|
+
`当前环境 target: ${result.currentTarget} (${result.kind}, ${result.appType})`
|
|
1894
|
+
);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (subcommand === 'list') {
|
|
1899
|
+
const profileName = flags.profile || config.currentProfile;
|
|
1900
|
+
const data = await requestWithAuth(
|
|
1901
|
+
config,
|
|
1902
|
+
profileName,
|
|
1903
|
+
environmentSetApiPath()
|
|
1904
|
+
);
|
|
1905
|
+
if (flags.json) return writeJson(data);
|
|
1906
|
+
print(JSON.stringify(data, null, 2));
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (subcommand === 'init') {
|
|
1911
|
+
const code = readStringFlag(flags, 'logical-app');
|
|
1912
|
+
const name = readStringFlag(flags, 'name');
|
|
1913
|
+
const kind = normalizeEnvironmentKind(
|
|
1914
|
+
flags.kind || positional[0] || 'preproduction'
|
|
1915
|
+
);
|
|
1916
|
+
const appType = readStringFlag(flags, 'app-type');
|
|
1917
|
+
const profileName = flags.profile || config.currentProfile;
|
|
1918
|
+
if (!code || !name || !appType || !profileName) {
|
|
1919
|
+
fail(
|
|
1920
|
+
'environment init 必须提供 --logical-app、--name、--app-type 和 --profile'
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const profile = getProfile(config, profileName);
|
|
1924
|
+
const sourceBase = readGitSourceBase(process.cwd(), 'HEAD');
|
|
1925
|
+
const sourceRepositoryId =
|
|
1926
|
+
readStringFlag(flags, 'source-repository-id') ||
|
|
1927
|
+
sourceBase.repositoryId ||
|
|
1928
|
+
sourceBase.repo;
|
|
1929
|
+
const sideEffectPolicy = flags['side-effect-policy-json']
|
|
1930
|
+
? readJsonInput(
|
|
1931
|
+
flags['side-effect-policy-json'],
|
|
1932
|
+
'side-effect-policy-json'
|
|
1933
|
+
)
|
|
1934
|
+
: defaultEnvironmentSideEffectPolicy(kind);
|
|
1935
|
+
const data = await requestWithAuth(
|
|
1936
|
+
config,
|
|
1937
|
+
profile.profileName,
|
|
1938
|
+
environmentSetApiPath(),
|
|
1939
|
+
{
|
|
1940
|
+
method: 'POST',
|
|
1941
|
+
body: {
|
|
1942
|
+
code,
|
|
1943
|
+
name,
|
|
1944
|
+
sourceRepositoryId,
|
|
1945
|
+
initialEnvironment: {
|
|
1946
|
+
kind,
|
|
1947
|
+
appType,
|
|
1948
|
+
displayName: flags['display-name'],
|
|
1949
|
+
publicOrigin: flags['public-origin'],
|
|
1950
|
+
sideEffectPolicy,
|
|
1951
|
+
},
|
|
1952
|
+
},
|
|
1953
|
+
}
|
|
1954
|
+
);
|
|
1955
|
+
const environmentDetail = (data.environments || []).find(
|
|
1956
|
+
item => item.kind === kind
|
|
1957
|
+
);
|
|
1958
|
+
if (!environmentDetail) {
|
|
1959
|
+
fail('平台创建环境组成功,但响应缺少初始 environment');
|
|
1960
|
+
}
|
|
1961
|
+
const bound = saveEnvironmentWorkspaceState(
|
|
1962
|
+
state,
|
|
1963
|
+
data,
|
|
1964
|
+
environmentDetail,
|
|
1965
|
+
{
|
|
1966
|
+
targetName: flags.target || flags.environment || kind,
|
|
1967
|
+
profile: profile.profileName,
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
const result = { logicalApp: state.logicalApp, target: bound };
|
|
1971
|
+
if (flags.json) return writeJson(result);
|
|
1972
|
+
print(
|
|
1973
|
+
`应用环境组已创建: ${state.logicalApp.code}; ${bound.targetName}=${bound.appType}`
|
|
1974
|
+
);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
if (subcommand === 'attach') {
|
|
1979
|
+
const code = readStringFlag(flags, 'logical-app');
|
|
1980
|
+
const profileName = flags.profile || config.currentProfile;
|
|
1981
|
+
if (!code || !profileName) {
|
|
1982
|
+
fail('environment attach 必须提供 --logical-app 和 --profile');
|
|
1983
|
+
}
|
|
1984
|
+
const profile = getProfile(config, profileName);
|
|
1985
|
+
const setDetail = await requestWithAuth(
|
|
1986
|
+
config,
|
|
1987
|
+
profile.profileName,
|
|
1988
|
+
environmentSetApiPath(code)
|
|
1989
|
+
);
|
|
1990
|
+
const environments = setDetail.environments || [];
|
|
1991
|
+
if (environments.length === 0) {
|
|
1992
|
+
fail(`应用环境组没有可绑定环境: ${code}`);
|
|
1993
|
+
}
|
|
1994
|
+
const targets = [];
|
|
1995
|
+
for (const environmentDetail of environments) {
|
|
1996
|
+
const targetName =
|
|
1997
|
+
flags[`${environmentDetail.kind}-target`] ||
|
|
1998
|
+
environmentDetail.kind;
|
|
1999
|
+
targets.push(
|
|
2000
|
+
saveEnvironmentWorkspaceState(
|
|
2001
|
+
state,
|
|
2002
|
+
setDetail,
|
|
2003
|
+
environmentDetail,
|
|
2004
|
+
{
|
|
2005
|
+
targetName,
|
|
2006
|
+
profile: profile.profileName,
|
|
2007
|
+
}
|
|
2008
|
+
)
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
const result = { logicalApp: state.logicalApp, targets };
|
|
2012
|
+
if (flags.json) return writeJson(result);
|
|
2013
|
+
print(
|
|
2014
|
+
`应用环境组已接入: ${state.logicalApp.code}; ${targets
|
|
2015
|
+
.map(target => `${target.targetName}=${target.appType}`)
|
|
2016
|
+
.join(', ')}`
|
|
2017
|
+
);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const code =
|
|
2022
|
+
readStringFlag(flags, 'logical-app') || state.logicalApp?.code;
|
|
2023
|
+
if (!code) {
|
|
2024
|
+
fail('当前工作区未登记 logical app,请先执行 openxiangda environment init');
|
|
2025
|
+
}
|
|
2026
|
+
const selectedProfile =
|
|
2027
|
+
flags.profile ||
|
|
2028
|
+
Object.values(state.targets || {})[0]?.profile ||
|
|
2029
|
+
config.currentProfile;
|
|
2030
|
+
|
|
2031
|
+
if (subcommand === 'bind') {
|
|
2032
|
+
const kind = normalizeEnvironmentKind(positional[0] || flags.kind);
|
|
2033
|
+
const appType = readStringFlag(flags, 'app-type');
|
|
2034
|
+
if (!appType) fail('environment bind 必须提供 --app-type');
|
|
2035
|
+
const profile = getProfile(config, selectedProfile);
|
|
2036
|
+
const sideEffectPolicy = flags['side-effect-policy-json']
|
|
2037
|
+
? readJsonInput(
|
|
2038
|
+
flags['side-effect-policy-json'],
|
|
2039
|
+
'side-effect-policy-json'
|
|
2040
|
+
)
|
|
2041
|
+
: defaultEnvironmentSideEffectPolicy(kind);
|
|
2042
|
+
const environmentDetail = await requestWithAuth(
|
|
2043
|
+
config,
|
|
2044
|
+
profile.profileName,
|
|
2045
|
+
environmentSetApiPath(code, '/environments'),
|
|
2046
|
+
{
|
|
2047
|
+
method: 'POST',
|
|
2048
|
+
body: {
|
|
2049
|
+
kind,
|
|
2050
|
+
appType,
|
|
2051
|
+
displayName: flags['display-name'],
|
|
2052
|
+
publicOrigin: flags['public-origin'],
|
|
2053
|
+
sideEffectPolicy,
|
|
2054
|
+
},
|
|
2055
|
+
}
|
|
2056
|
+
);
|
|
2057
|
+
const setDetail = await requestWithAuth(
|
|
2058
|
+
config,
|
|
2059
|
+
profile.profileName,
|
|
2060
|
+
environmentSetApiPath(code)
|
|
2061
|
+
);
|
|
2062
|
+
const bound = saveEnvironmentWorkspaceState(
|
|
2063
|
+
state,
|
|
2064
|
+
setDetail,
|
|
2065
|
+
environmentDetail,
|
|
2066
|
+
{
|
|
2067
|
+
targetName: flags.target || flags.environment || kind,
|
|
2068
|
+
profile: profile.profileName,
|
|
2069
|
+
}
|
|
2070
|
+
);
|
|
2071
|
+
if (flags.json) return writeJson(bound);
|
|
2072
|
+
print(`应用环境已绑定: ${bound.targetName}=${bound.appType}`);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
if (subcommand === 'status' || subcommand === 'diff') {
|
|
2077
|
+
const data = await requestWithAuth(
|
|
2078
|
+
config,
|
|
2079
|
+
selectedProfile,
|
|
2080
|
+
environmentSetApiPath(code, '/status')
|
|
2081
|
+
);
|
|
2082
|
+
const result =
|
|
2083
|
+
subcommand === 'diff'
|
|
2084
|
+
? buildEnvironmentStatusDiff(data)
|
|
2085
|
+
: data;
|
|
2086
|
+
if (flags.json) return writeJson(result);
|
|
2087
|
+
print(JSON.stringify(result, null, 2));
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
fail('用法: openxiangda environment init|attach|bind|list|status|diff|use');
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
function buildEnvironmentStatusDiff(status) {
|
|
2095
|
+
const environments = status?.environments || [];
|
|
2096
|
+
const preproduction = environments.find(
|
|
2097
|
+
item => item.kind === 'preproduction'
|
|
2098
|
+
);
|
|
2099
|
+
const production = environments.find(item => item.kind === 'production');
|
|
2100
|
+
return {
|
|
2101
|
+
logicalApp: {
|
|
2102
|
+
id: status?.id || null,
|
|
2103
|
+
code: status?.code || null,
|
|
2104
|
+
name: status?.name || null,
|
|
2105
|
+
},
|
|
2106
|
+
preproduction: preproduction || null,
|
|
2107
|
+
production: production || null,
|
|
2108
|
+
drift: {
|
|
2109
|
+
sameAppRelease:
|
|
2110
|
+
Boolean(preproduction?.heads?.appReleaseId) &&
|
|
2111
|
+
preproduction?.heads?.appReleaseId ===
|
|
2112
|
+
production?.heads?.appReleaseId,
|
|
2113
|
+
candidate:
|
|
2114
|
+
preproduction?.latestDeployment?.candidateId || null,
|
|
2115
|
+
promotedCandidate:
|
|
2116
|
+
production?.latestDeployment?.candidateId || null,
|
|
2117
|
+
promotionReady:
|
|
2118
|
+
preproduction?.latestDeployment?.status === 'succeeded' &&
|
|
2119
|
+
preproduction?.latestDeployment?.candidateId !== null &&
|
|
2120
|
+
preproduction?.latestDeployment?.candidateId !== undefined,
|
|
2121
|
+
},
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function readStudioGit(args) {
|
|
2126
|
+
const result = spawnSync('git', args, {
|
|
2127
|
+
cwd: process.cwd(),
|
|
2128
|
+
encoding: 'utf8',
|
|
2129
|
+
});
|
|
2130
|
+
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
async function studio(args) {
|
|
2134
|
+
const { flags } = parseArgs(args);
|
|
2135
|
+
const config = loadConfig();
|
|
2136
|
+
const initialState = loadProjectState();
|
|
2137
|
+
if (!initialState.logicalApp?.code) {
|
|
2138
|
+
fail(
|
|
2139
|
+
'Developer Center 需要环境管理工作区;请先执行 openxiangda environment init'
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
const getStatus = async () => {
|
|
2143
|
+
const state = loadProjectState();
|
|
2144
|
+
const selected = resolveEnvironmentTarget(
|
|
2145
|
+
state,
|
|
2146
|
+
state.currentTarget || 'preproduction'
|
|
2147
|
+
);
|
|
2148
|
+
const remote = await requestWithAuth(
|
|
2149
|
+
config,
|
|
2150
|
+
selected.binding.profile,
|
|
2151
|
+
environmentSetApiPath(state.logicalApp.code, '/status')
|
|
2152
|
+
);
|
|
2153
|
+
const porcelain = readStudioGit([
|
|
2154
|
+
'status',
|
|
2155
|
+
'--porcelain',
|
|
2156
|
+
'--untracked-files=all',
|
|
2157
|
+
]);
|
|
2158
|
+
return {
|
|
2159
|
+
workspace: process.cwd(),
|
|
2160
|
+
logicalApp: state.logicalApp,
|
|
2161
|
+
currentTarget: state.currentTarget,
|
|
2162
|
+
targets: state.targets,
|
|
2163
|
+
git: {
|
|
2164
|
+
branch: readStudioGit(['branch', '--show-current']),
|
|
2165
|
+
commit: readStudioGit(['rev-parse', 'HEAD']),
|
|
2166
|
+
clean: !porcelain,
|
|
2167
|
+
changes: porcelain ? porcelain.split(/\r?\n/).slice(0, 50) : [],
|
|
2168
|
+
},
|
|
2169
|
+
openxiangdaVersion: CURRENT_VERSION,
|
|
2170
|
+
remote,
|
|
2171
|
+
};
|
|
2172
|
+
};
|
|
2173
|
+
const runAction = async body => {
|
|
2174
|
+
const state = loadProjectState();
|
|
2175
|
+
const action = String(body?.action || '').trim();
|
|
2176
|
+
const value = String(body?.value || '').trim();
|
|
2177
|
+
const details = String(body?.details || '').trim();
|
|
2178
|
+
const pre = resolveEnvironmentTarget(state, 'preproduction');
|
|
2179
|
+
const production =
|
|
2180
|
+
action === 'promote' || action === 'rollback'
|
|
2181
|
+
? resolveEnvironmentTarget(state, 'production')
|
|
2182
|
+
: null;
|
|
2183
|
+
if (action === 'candidate') {
|
|
2184
|
+
if (!value) throw new Error('请输入 OpenSpec changeId');
|
|
2185
|
+
return await runOpenXiangdaInProcess([
|
|
2186
|
+
'release',
|
|
2187
|
+
'candidate',
|
|
2188
|
+
'--change',
|
|
2189
|
+
value,
|
|
2190
|
+
'--environment',
|
|
2191
|
+
pre.targetName,
|
|
2192
|
+
]);
|
|
2193
|
+
}
|
|
2194
|
+
if (action === 'deploy') {
|
|
2195
|
+
if (!value) throw new Error('请输入 candidateId');
|
|
2196
|
+
return await runOpenXiangdaInProcess([
|
|
2197
|
+
'release',
|
|
2198
|
+
'deploy',
|
|
2199
|
+
'--candidate',
|
|
2200
|
+
value,
|
|
2201
|
+
'--environment',
|
|
2202
|
+
pre.targetName,
|
|
2203
|
+
]);
|
|
2204
|
+
}
|
|
2205
|
+
if (action === 'test') {
|
|
2206
|
+
if (!details) throw new Error('请输入测试证据 JSON');
|
|
2207
|
+
return await runOpenXiangdaInProcess([
|
|
2208
|
+
'release',
|
|
2209
|
+
'test',
|
|
2210
|
+
...(value ? ['--deployment', value] : []),
|
|
2211
|
+
'--evidence-json',
|
|
2212
|
+
details,
|
|
2213
|
+
'--environment',
|
|
2214
|
+
pre.targetName,
|
|
2215
|
+
]);
|
|
2216
|
+
}
|
|
2217
|
+
if (action === 'promote') {
|
|
2218
|
+
if (!body?.confirmProduction) {
|
|
2219
|
+
throw new Error('production action requires explicit confirmation');
|
|
2220
|
+
}
|
|
2221
|
+
if (!value) throw new Error('请输入 candidateId');
|
|
2222
|
+
return await runOpenXiangdaInProcess([
|
|
2223
|
+
'release',
|
|
2224
|
+
'promote',
|
|
2225
|
+
'--candidate',
|
|
2226
|
+
value,
|
|
2227
|
+
'--environment',
|
|
2228
|
+
production.targetName,
|
|
2229
|
+
'--confirm-production',
|
|
2230
|
+
]);
|
|
2231
|
+
}
|
|
2232
|
+
if (action === 'rollback') {
|
|
2233
|
+
if (!body?.confirmProduction) {
|
|
2234
|
+
throw new Error('production action requires explicit confirmation');
|
|
2235
|
+
}
|
|
2236
|
+
if (!value || details.length < 8) {
|
|
2237
|
+
throw new Error('请输入目标 AppReleaseId 和至少 8 个字符的回退原因');
|
|
2238
|
+
}
|
|
2239
|
+
return await runOpenXiangdaInProcess([
|
|
2240
|
+
'release',
|
|
2241
|
+
'rollback',
|
|
2242
|
+
'--to',
|
|
2243
|
+
value,
|
|
2244
|
+
'--reason',
|
|
2245
|
+
details,
|
|
2246
|
+
'--environment',
|
|
2247
|
+
production.targetName,
|
|
2248
|
+
'--confirm-production',
|
|
2249
|
+
]);
|
|
2250
|
+
}
|
|
2251
|
+
throw new Error(`未注册的 Developer Center 操作: ${action}`);
|
|
2252
|
+
};
|
|
2253
|
+
const port = flags.port === undefined ? 0 : Number(flags.port);
|
|
2254
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
2255
|
+
fail('studio --port 必须是 0-65535 的整数');
|
|
2256
|
+
}
|
|
2257
|
+
const center = await startDeveloperCenter({ port, getStatus, runAction });
|
|
2258
|
+
if (!flags['no-open']) openBrowser(center.url);
|
|
2259
|
+
const publicUrl = center.url.replace(/\?.*$/, '');
|
|
2260
|
+
print(
|
|
2261
|
+
flags['no-open']
|
|
2262
|
+
? `OpenXiangda Developer Center: ${center.url}`
|
|
2263
|
+
: `OpenXiangda Developer Center 已启动: ${publicUrl}`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
function requireLogicalApplication(target) {
|
|
2268
|
+
const logicalApp = target.logicalApp || target.state?.logicalApp;
|
|
2269
|
+
if (!logicalApp?.code || !logicalApp?.id) {
|
|
2270
|
+
fail(
|
|
2271
|
+
'当前工作区尚未登记 logical app;请先执行 openxiangda environment init'
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
return logicalApp;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
function applicationDeploymentApiPath(logicalAppCode, deploymentId, action) {
|
|
2278
|
+
const suffix = deploymentId
|
|
2279
|
+
? `/deployments/${encodeURIComponent(deploymentId)}${
|
|
2280
|
+
action ? `/${action}` : ''
|
|
2281
|
+
}`
|
|
2282
|
+
: '/deployments';
|
|
2283
|
+
return environmentSetApiPath(logicalAppCode, suffix);
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function rememberTargetDeployment(target, deployment) {
|
|
2287
|
+
target.bound.lastDeploymentId = deployment.id;
|
|
2288
|
+
target.bound.lastCandidateId = deployment.candidateId || null;
|
|
2289
|
+
target.bound.lastDeploymentStatus = deployment.status;
|
|
2290
|
+
target.bound.updatedAt = new Date().toISOString();
|
|
2291
|
+
saveProjectState(target.state);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
function assertLocalCandidate(candidate) {
|
|
2295
|
+
if (
|
|
2296
|
+
!candidate?.id ||
|
|
2297
|
+
!candidate?.candidateHash ||
|
|
2298
|
+
!candidate?.sourceBundleManifest ||
|
|
2299
|
+
sha256Canonical(candidate.sourceBundleManifest) !== candidate.candidateHash
|
|
2300
|
+
) {
|
|
2301
|
+
fail('LOCAL_CANDIDATE_INVALID: 本地 candidate 文件缺失或哈希不一致');
|
|
2302
|
+
}
|
|
2303
|
+
assertReleaseSourceRevisionStable(candidate.sourceRevision, {
|
|
2304
|
+
cwd: process.cwd(),
|
|
2305
|
+
});
|
|
2306
|
+
return candidate;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
async function createApplicationCandidate(config, target, flags, positional) {
|
|
2310
|
+
const logicalApp = requireLogicalApplication(target);
|
|
2311
|
+
const changeId =
|
|
2312
|
+
readStringFlag(flags, 'change') || positional[0];
|
|
2313
|
+
if (!changeId) {
|
|
2314
|
+
fail('用法: openxiangda release candidate --change <id>');
|
|
2315
|
+
}
|
|
2316
|
+
prepareReleaseSourceRevision({ cwd: process.cwd() });
|
|
2317
|
+
const sourceRevision = readGitSourceBase(process.cwd(), 'HEAD');
|
|
2318
|
+
if (
|
|
2319
|
+
logicalApp.sourceRepositoryId !==
|
|
2320
|
+
(sourceRevision.repositoryId || sourceRevision.repo)
|
|
2321
|
+
) {
|
|
2322
|
+
fail(
|
|
2323
|
+
'CANDIDATE_REPOSITORY_MISMATCH: 当前 Git 仓库与 logical app 权威仓库不一致'
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
const plan = buildWorkspaceReleasePlan({
|
|
2327
|
+
changed: true,
|
|
2328
|
+
since: flags.since,
|
|
2329
|
+
profile: target.profileName,
|
|
2330
|
+
skipSdd: true,
|
|
2331
|
+
changeId,
|
|
2332
|
+
expandDependencies: Boolean(flags['expand-dependencies']),
|
|
2333
|
+
});
|
|
2334
|
+
if (plan.errors.length > 0) {
|
|
2335
|
+
fail(
|
|
2336
|
+
`RELEASE_PLAN_INVALID: ${plan.errors
|
|
2337
|
+
.map(error => error.message || error.name)
|
|
2338
|
+
.join('; ')}`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
const verification = verifySddChange({
|
|
2342
|
+
cwd: process.cwd(),
|
|
2343
|
+
changeId,
|
|
2344
|
+
releasePlan: plan,
|
|
2345
|
+
writeMetadata: false,
|
|
2346
|
+
stage: 'prepublish',
|
|
2347
|
+
});
|
|
2348
|
+
if (!verification.passed) {
|
|
2349
|
+
fail(
|
|
2350
|
+
`SDD_PREPUBLISH_FAILED: ${verification.errors
|
|
2351
|
+
.map(error => error.message)
|
|
2352
|
+
.join('; ')}`
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
const testPlan = flags['test-plan-json']
|
|
2356
|
+
? readJsonInput(flags['test-plan-json'], 'test-plan-json')
|
|
2357
|
+
: undefined;
|
|
2358
|
+
const bundle = buildCandidateBundle({
|
|
2359
|
+
cwd: process.cwd(),
|
|
2360
|
+
logicalAppCode: logicalApp.code,
|
|
2361
|
+
changeId,
|
|
2362
|
+
sourceRevision,
|
|
2363
|
+
runtimeMode: plan.workspace.runtimeMode,
|
|
2364
|
+
targets: plan.activationTargets,
|
|
2365
|
+
files: plan.changedFiles,
|
|
2366
|
+
testPlan,
|
|
2367
|
+
openxiangdaVersion: CURRENT_VERSION,
|
|
2368
|
+
});
|
|
2369
|
+
const data = await requestWithAuth(
|
|
2370
|
+
config,
|
|
2371
|
+
target.profileName,
|
|
2372
|
+
environmentSetApiPath(logicalApp.code, '/candidates'),
|
|
2373
|
+
{
|
|
2374
|
+
method: 'POST',
|
|
2375
|
+
body: {
|
|
2376
|
+
candidateHash: bundle.candidateHash,
|
|
2377
|
+
changeId,
|
|
2378
|
+
sourceRepositoryId: logicalApp.sourceRepositoryId,
|
|
2379
|
+
sourceRevision,
|
|
2380
|
+
sourceBundleManifest: bundle.sourceBundleManifest,
|
|
2381
|
+
runtimeArtifactHash: null,
|
|
2382
|
+
runtimeArtifactLocator: null,
|
|
2383
|
+
testPlanHash: bundle.testPlanHash,
|
|
2384
|
+
openxiangdaVersion: CURRENT_VERSION,
|
|
2385
|
+
},
|
|
2386
|
+
}
|
|
2387
|
+
);
|
|
2388
|
+
const localCandidate = {
|
|
2389
|
+
...data,
|
|
2390
|
+
logicalAppCode: logicalApp.code,
|
|
2391
|
+
sourceRevision,
|
|
2392
|
+
sourceBundleManifest: bundle.sourceBundleManifest,
|
|
2393
|
+
testPlan: bundle.testPlan,
|
|
2394
|
+
releasePlan: plan,
|
|
2395
|
+
savedAt: new Date().toISOString(),
|
|
2396
|
+
};
|
|
2397
|
+
const file = saveCandidate(localCandidate);
|
|
2398
|
+
return {
|
|
2399
|
+
candidate: localCandidate,
|
|
2400
|
+
candidateFile: path.relative(process.cwd(), file).replace(/\\/g, '/'),
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
async function markApplicationDeploymentFailed(
|
|
2405
|
+
config,
|
|
2406
|
+
target,
|
|
2407
|
+
logicalAppCode,
|
|
2408
|
+
deploymentId,
|
|
2409
|
+
error
|
|
2410
|
+
) {
|
|
2411
|
+
try {
|
|
2412
|
+
await requestWithAuth(
|
|
2413
|
+
config,
|
|
2414
|
+
target.profileName,
|
|
2415
|
+
applicationDeploymentApiPath(
|
|
2416
|
+
logicalAppCode,
|
|
2417
|
+
deploymentId,
|
|
2418
|
+
'fail'
|
|
2419
|
+
),
|
|
2420
|
+
{
|
|
2421
|
+
method: 'POST',
|
|
2422
|
+
body: {
|
|
2423
|
+
code: error?.code || 'OPENXIANGDA_DEPLOYMENT_FAILED',
|
|
2424
|
+
message: maskText(error?.message || String(error)).slice(0, 2000),
|
|
2425
|
+
details: {
|
|
2426
|
+
command: 'openxiangda release',
|
|
2427
|
+
recordedAt: new Date().toISOString(),
|
|
2428
|
+
},
|
|
2429
|
+
},
|
|
2430
|
+
}
|
|
2431
|
+
);
|
|
2432
|
+
} catch (recordError) {
|
|
2433
|
+
warn(
|
|
2434
|
+
`部署失败记录未写入平台: ${maskText(recordError?.message || recordError)}`
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
async function deployApplicationCandidate(
|
|
2440
|
+
config,
|
|
2441
|
+
target,
|
|
2442
|
+
candidate,
|
|
2443
|
+
options = {}
|
|
2444
|
+
) {
|
|
2445
|
+
const logicalApp = requireLogicalApplication(target);
|
|
2446
|
+
assertLocalCandidate(candidate);
|
|
2447
|
+
const kind = options.kind || 'deploy';
|
|
2448
|
+
const body = {
|
|
2449
|
+
kind,
|
|
2450
|
+
targetEnvironmentId: target.environmentId,
|
|
2451
|
+
...(options.sourceEnvironmentId
|
|
2452
|
+
? { sourceEnvironmentId: options.sourceEnvironmentId }
|
|
2453
|
+
: {}),
|
|
2454
|
+
...(kind === 'rollback'
|
|
2455
|
+
? { rollbackTargetReleaseId: options.rollbackTargetReleaseId }
|
|
2456
|
+
: { candidateId: candidate.id }),
|
|
2457
|
+
};
|
|
2458
|
+
const deployment = await requestWithAuth(
|
|
2459
|
+
config,
|
|
2460
|
+
target.profileName,
|
|
2461
|
+
applicationDeploymentApiPath(logicalApp.code),
|
|
2462
|
+
{ method: 'POST', body }
|
|
2463
|
+
);
|
|
2464
|
+
rememberTargetDeployment(target, deployment);
|
|
2465
|
+
try {
|
|
2466
|
+
const published = await publishWorkspaceRelease(config, target, {
|
|
2467
|
+
...options.flags,
|
|
2468
|
+
change: candidate.changeId,
|
|
2469
|
+
environment: target.targetName,
|
|
2470
|
+
'candidate-id': candidate.id,
|
|
2471
|
+
'deployment-id': deployment.id,
|
|
2472
|
+
});
|
|
2473
|
+
const detail = await requestWithAuth(
|
|
2474
|
+
config,
|
|
2475
|
+
target.profileName,
|
|
2476
|
+
applicationDeploymentApiPath(logicalApp.code, deployment.id)
|
|
2477
|
+
);
|
|
2478
|
+
rememberTargetDeployment(target, detail);
|
|
2479
|
+
return { deployment: detail, published };
|
|
2480
|
+
} catch (error) {
|
|
2481
|
+
await markApplicationDeploymentFailed(
|
|
2482
|
+
config,
|
|
2483
|
+
target,
|
|
2484
|
+
logicalApp.code,
|
|
2485
|
+
deployment.id,
|
|
2486
|
+
error
|
|
2487
|
+
);
|
|
2488
|
+
throw error;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function buildBoundDeploymentEvidence(input, target, deployment, candidate) {
|
|
2493
|
+
const source = input || {};
|
|
2494
|
+
const finishedAt = source.finishedAt || new Date().toISOString();
|
|
2495
|
+
const finishedAtMs = Date.parse(finishedAt);
|
|
2496
|
+
if (!Number.isFinite(finishedAtMs)) {
|
|
2497
|
+
fail('测试证据 finishedAt 无效');
|
|
2498
|
+
}
|
|
2499
|
+
const validUntil =
|
|
2500
|
+
source.validUntil ||
|
|
2501
|
+
new Date(finishedAtMs + 24 * 60 * 60 * 1000).toISOString();
|
|
2502
|
+
return {
|
|
2503
|
+
...source,
|
|
2504
|
+
schemaVersion: 1,
|
|
2505
|
+
deploymentId: deployment.id,
|
|
2506
|
+
candidateId: candidate?.id || null,
|
|
2507
|
+
candidateHash: candidate?.candidateHash || null,
|
|
2508
|
+
testPlanHash: candidate?.testPlanHash || null,
|
|
2509
|
+
environmentId: target.environmentId,
|
|
2510
|
+
environmentKind: target.environmentKind,
|
|
2511
|
+
appType: target.appType,
|
|
2512
|
+
appReleaseId: deployment.targetAppReleaseId,
|
|
2513
|
+
outcome: source.outcome || 'passed',
|
|
2514
|
+
startedAt: source.startedAt || finishedAt,
|
|
2515
|
+
finishedAt,
|
|
2516
|
+
validUntil,
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
async function runApplicationEnvironmentReleaseCommand(
|
|
2521
|
+
config,
|
|
2522
|
+
target,
|
|
2523
|
+
subcommand,
|
|
2524
|
+
positional,
|
|
2525
|
+
flags
|
|
2526
|
+
) {
|
|
2527
|
+
const logicalApp = requireLogicalApplication(target);
|
|
2528
|
+
if (subcommand === 'candidate') {
|
|
2529
|
+
return await createApplicationCandidate(
|
|
2530
|
+
config,
|
|
2531
|
+
target,
|
|
2532
|
+
flags,
|
|
2533
|
+
positional
|
|
2534
|
+
);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const candidateId =
|
|
2538
|
+
readStringFlag(flags, 'candidate') ||
|
|
2539
|
+
readStringFlag(flags, 'candidate-id') ||
|
|
2540
|
+
positional[0] ||
|
|
2541
|
+
target.bound.lastCandidateId;
|
|
2542
|
+
if (subcommand === 'deploy') {
|
|
2543
|
+
if (target.environmentKind !== 'preproduction') {
|
|
2544
|
+
fail('release deploy 只允许目标为 preproduction');
|
|
2545
|
+
}
|
|
2546
|
+
if (!candidateId) fail('release deploy 必须提供 --candidate');
|
|
2547
|
+
return await deployApplicationCandidate(
|
|
2548
|
+
config,
|
|
2549
|
+
target,
|
|
2550
|
+
readCandidate(candidateId),
|
|
2551
|
+
{ kind: 'deploy', flags }
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
if (subcommand === 'promote') {
|
|
2556
|
+
if (target.environmentKind !== 'production') {
|
|
2557
|
+
fail(
|
|
2558
|
+
'release promote 必须使用 --environment production(或对应 production target)'
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
if (!flags['confirm-production']) {
|
|
2562
|
+
fail('正式晋级必须显式提供 --confirm-production');
|
|
2563
|
+
}
|
|
2564
|
+
if (!candidateId) fail('release promote 必须提供 --candidate');
|
|
2565
|
+
const source = resolveEnvironmentTarget(target.state, 'preproduction');
|
|
2566
|
+
return await deployApplicationCandidate(
|
|
2567
|
+
config,
|
|
2568
|
+
target,
|
|
2569
|
+
readCandidate(candidateId),
|
|
2570
|
+
{
|
|
2571
|
+
kind: 'promotion',
|
|
2572
|
+
sourceEnvironmentId: source.binding.environmentId,
|
|
2573
|
+
flags,
|
|
2574
|
+
}
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
if (subcommand === 'test') {
|
|
2579
|
+
const deploymentId =
|
|
2580
|
+
readStringFlag(flags, 'deployment') ||
|
|
2581
|
+
readStringFlag(flags, 'deployment-id') ||
|
|
2582
|
+
target.bound.lastDeploymentId;
|
|
2583
|
+
if (!deploymentId || !flags['evidence-json']) {
|
|
2584
|
+
fail(
|
|
2585
|
+
'release test 必须提供 --deployment <id>(首次可省略)和 --evidence-json <JSON|file>'
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
const deployment = await requestWithAuth(
|
|
2589
|
+
config,
|
|
2590
|
+
target.profileName,
|
|
2591
|
+
applicationDeploymentApiPath(logicalApp.code, deploymentId)
|
|
2592
|
+
);
|
|
2593
|
+
const candidate = deployment.candidateId
|
|
2594
|
+
? readCandidate(deployment.candidateId)
|
|
2595
|
+
: null;
|
|
2596
|
+
const evidence = buildBoundDeploymentEvidence(
|
|
2597
|
+
readJsonInput(flags['evidence-json'], 'evidence-json'),
|
|
2598
|
+
target,
|
|
2599
|
+
deployment,
|
|
2600
|
+
candidate
|
|
2601
|
+
);
|
|
2602
|
+
const completed = await requestWithAuth(
|
|
2603
|
+
config,
|
|
2604
|
+
target.profileName,
|
|
2605
|
+
applicationDeploymentApiPath(logicalApp.code, deploymentId, 'complete'),
|
|
2606
|
+
{
|
|
2607
|
+
method: 'POST',
|
|
2608
|
+
body: {
|
|
2609
|
+
evidenceSummary: evidence,
|
|
2610
|
+
evidenceHash: sha256Canonical(evidence),
|
|
2611
|
+
},
|
|
2612
|
+
}
|
|
2613
|
+
);
|
|
2614
|
+
rememberTargetDeployment(target, completed);
|
|
2615
|
+
return { deployment: completed, evidence };
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if (subcommand === 'rollback') {
|
|
2619
|
+
if (target.environmentKind !== 'production') {
|
|
2620
|
+
fail('release rollback 只允许 production target');
|
|
2621
|
+
}
|
|
2622
|
+
if (!flags['confirm-production']) {
|
|
2623
|
+
fail('正式回退必须显式提供 --confirm-production');
|
|
2624
|
+
}
|
|
2625
|
+
const rollbackTargetReleaseId =
|
|
2626
|
+
readStringFlag(flags, 'to') ||
|
|
2627
|
+
readStringFlag(flags, 'release-id') ||
|
|
2628
|
+
positional[0];
|
|
2629
|
+
const reason = readStringFlag(flags, 'reason');
|
|
2630
|
+
if (!rollbackTargetReleaseId || reason.length < 8) {
|
|
2631
|
+
fail(
|
|
2632
|
+
'release rollback 必须提供 --to <AppReleaseId> 和至少 8 个字符的 --reason'
|
|
2633
|
+
);
|
|
2634
|
+
}
|
|
2635
|
+
const deployment = await requestWithAuth(
|
|
2636
|
+
config,
|
|
2637
|
+
target.profileName,
|
|
2638
|
+
applicationDeploymentApiPath(logicalApp.code),
|
|
2639
|
+
{
|
|
2640
|
+
method: 'POST',
|
|
2641
|
+
body: {
|
|
2642
|
+
kind: 'rollback',
|
|
2643
|
+
targetEnvironmentId: target.environmentId,
|
|
2644
|
+
rollbackTargetReleaseId,
|
|
2645
|
+
},
|
|
2646
|
+
}
|
|
2647
|
+
);
|
|
2648
|
+
rememberTargetDeployment(target, deployment);
|
|
2649
|
+
return {
|
|
2650
|
+
deployment,
|
|
2651
|
+
prepared: true,
|
|
2652
|
+
automaticActivation: false,
|
|
2653
|
+
reason:
|
|
2654
|
+
'总 AppRelease 回退记录已冻结;各历史子 Release 仍需生成新的审计式 rollback Release 后再原子激活,CLI 不会直接重激活旧行。',
|
|
2655
|
+
nextStep: `openxiangda release app-rollback ${rollbackTargetReleaseId} --change rollback-${deployment.id} --reason ${JSON.stringify(
|
|
2656
|
+
reason
|
|
2657
|
+
)} --environment ${target.targetName}`,
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
fail(`未知应用环境 Release 命令: ${subcommand}`);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
1709
2664
|
async function release(args) {
|
|
1710
2665
|
const { subcommand, rest } = parseSubcommandArgs(args);
|
|
1711
2666
|
const { flags, positional } = parseArgs(rest);
|
|
1712
2667
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
1713
2668
|
print([
|
|
1714
2669
|
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
2670
|
+
' openxiangda release candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--confirm-production] [--json]',
|
|
1715
2671
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
1716
2672
|
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
|
|
1717
2673
|
'常用流程:',
|
|
@@ -1748,6 +2704,22 @@ async function release(args) {
|
|
|
1748
2704
|
flags.profile || config.currentProfile,
|
|
1749
2705
|
flags
|
|
1750
2706
|
);
|
|
2707
|
+
if (
|
|
2708
|
+
['candidate', 'deploy', 'test', 'promote', 'rollback'].includes(
|
|
2709
|
+
subcommand
|
|
2710
|
+
)
|
|
2711
|
+
) {
|
|
2712
|
+
const result = await runApplicationEnvironmentReleaseCommand(
|
|
2713
|
+
config,
|
|
2714
|
+
target,
|
|
2715
|
+
subcommand,
|
|
2716
|
+
positional,
|
|
2717
|
+
flags
|
|
2718
|
+
);
|
|
2719
|
+
if (flags.json) return writeJson(result);
|
|
2720
|
+
print(JSON.stringify(result, null, 2));
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
1751
2723
|
if (String(subcommand || '').startsWith('backend-')) {
|
|
1752
2724
|
return await runBackendReleaseCommand(
|
|
1753
2725
|
config,
|
|
@@ -1767,6 +2739,16 @@ async function release(args) {
|
|
|
1767
2739
|
);
|
|
1768
2740
|
}
|
|
1769
2741
|
if (subcommand === 'publish') {
|
|
2742
|
+
if (target.environmentKind === 'production') {
|
|
2743
|
+
fail(
|
|
2744
|
+
'PRODUCTION_PROMOTION_REQUIRED: production target 禁止 release publish;请使用 release promote --candidate <id> --environment production --confirm-production'
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
if (target.environmentKind === 'preproduction') {
|
|
2748
|
+
fail(
|
|
2749
|
+
'PREPRODUCTION_DEPLOYMENT_REQUIRED: 环境管理工作区请使用 release candidate + release deploy,确保候选和 deployment 可追踪'
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
1770
2752
|
const result = await publishWorkspaceRelease(config, target, flags);
|
|
1771
2753
|
if (flags.json) return writeJson(result);
|
|
1772
2754
|
print(
|
|
@@ -1783,7 +2765,10 @@ async function release(args) {
|
|
|
1783
2765
|
if (!changeId) {
|
|
1784
2766
|
fail('用法: openxiangda release explain --change <id> [--json]');
|
|
1785
2767
|
}
|
|
1786
|
-
const execution = readReleaseExecution(
|
|
2768
|
+
const execution = readReleaseExecution(
|
|
2769
|
+
changeId,
|
|
2770
|
+
readStringFlag(flags, 'deployment-id')
|
|
2771
|
+
);
|
|
1787
2772
|
const result = {
|
|
1788
2773
|
changeId,
|
|
1789
2774
|
execution,
|
|
@@ -1821,7 +2806,12 @@ async function release(args) {
|
|
|
1821
2806
|
readStringFlag(flags, 'change') ||
|
|
1822
2807
|
local?.changeId ||
|
|
1823
2808
|
baseline?.changeId;
|
|
1824
|
-
const execution = changeId
|
|
2809
|
+
const execution = changeId
|
|
2810
|
+
? readReleaseExecution(
|
|
2811
|
+
changeId,
|
|
2812
|
+
readStringFlag(flags, 'deployment-id')
|
|
2813
|
+
)
|
|
2814
|
+
: null;
|
|
1825
2815
|
const result = {
|
|
1826
2816
|
remote,
|
|
1827
2817
|
local,
|
|
@@ -2672,8 +3662,9 @@ async function fetchAppReleaseCapture(config, target, flags = {}) {
|
|
|
2672
3662
|
return clonePlainJson(capture);
|
|
2673
3663
|
}
|
|
2674
3664
|
|
|
2675
|
-
function releaseSessionFile(changeId) {
|
|
2676
|
-
|
|
3665
|
+
function releaseSessionFile(changeId, deploymentId) {
|
|
3666
|
+
const executionFile = releaseExecutionFile(changeId, deploymentId);
|
|
3667
|
+
return path.join(path.dirname(executionFile), 'release-session.json');
|
|
2677
3668
|
}
|
|
2678
3669
|
|
|
2679
3670
|
function saveFrozenAppReleaseCapture(
|
|
@@ -2683,12 +3674,13 @@ function saveFrozenAppReleaseCapture(
|
|
|
2683
3674
|
lease,
|
|
2684
3675
|
capture
|
|
2685
3676
|
) {
|
|
2686
|
-
const file = releaseSessionFile(changeId);
|
|
3677
|
+
const file = releaseSessionFile(changeId, target.deploymentId);
|
|
2687
3678
|
writePrivateJsonAtomic(file, {
|
|
2688
3679
|
schemaVersion: 'openxiangda_release_session_v1',
|
|
2689
3680
|
appType: target.appType,
|
|
2690
3681
|
profile: target.profileName,
|
|
2691
3682
|
changeId,
|
|
3683
|
+
deploymentId: target.deploymentId || null,
|
|
2692
3684
|
baselineId: baseline?.baselineId || baseline?.id || null,
|
|
2693
3685
|
leaseId: lease?.leaseId || lease?.id || null,
|
|
2694
3686
|
sourceRevision: baseline?.releaseSourceRevision || null,
|
|
@@ -2701,7 +3693,10 @@ function saveFrozenAppReleaseCapture(
|
|
|
2701
3693
|
function readFrozenAppReleaseCapture(target, flags = {}) {
|
|
2702
3694
|
const changeId = readStringFlag(flags, 'change');
|
|
2703
3695
|
if (!changeId) return null;
|
|
2704
|
-
const file = releaseSessionFile(
|
|
3696
|
+
const file = releaseSessionFile(
|
|
3697
|
+
changeId,
|
|
3698
|
+
readStringFlag(flags, 'deployment-id') || target.deploymentId
|
|
3699
|
+
);
|
|
2705
3700
|
if (!fs.existsSync(file)) return null;
|
|
2706
3701
|
let session;
|
|
2707
3702
|
try {
|
|
@@ -2713,7 +3708,9 @@ function readFrozenAppReleaseCapture(target, flags = {}) {
|
|
|
2713
3708
|
session?.schemaVersion !== 'openxiangda_release_session_v1' ||
|
|
2714
3709
|
session?.appType !== target.appType ||
|
|
2715
3710
|
session?.profile !== target.profileName ||
|
|
2716
|
-
session?.changeId !== changeId
|
|
3711
|
+
session?.changeId !== changeId ||
|
|
3712
|
+
(session?.deploymentId || null) !==
|
|
3713
|
+
(readStringFlag(flags, 'deployment-id') || target.deploymentId || null)
|
|
2717
3714
|
) {
|
|
2718
3715
|
fail(
|
|
2719
3716
|
'RELEASE_SESSION_CONTEXT_MISMATCH: 冻结的 App capture 与当前 app/profile/change 不一致'
|
|
@@ -2869,22 +3866,25 @@ function readStagedAppReleaseResources(flags = {}) {
|
|
|
2869
3866
|
);
|
|
2870
3867
|
}
|
|
2871
3868
|
|
|
2872
|
-
function stagedResourcesFileForChange(changeId) {
|
|
3869
|
+
function stagedResourcesFileForChange(changeId, deploymentId) {
|
|
2873
3870
|
const normalized = String(changeId || '').trim();
|
|
2874
3871
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(normalized)) {
|
|
2875
3872
|
fail('APP_RELEASE_STAGED_RESOURCES_CHANGE_INVALID: 自动聚合 staged resources 需要有效 --change');
|
|
2876
3873
|
}
|
|
2877
|
-
|
|
3874
|
+
const base = path.join(
|
|
2878
3875
|
process.cwd(),
|
|
2879
3876
|
'.openxiangda',
|
|
2880
3877
|
'releases',
|
|
2881
|
-
normalized
|
|
3878
|
+
normalized
|
|
3879
|
+
);
|
|
3880
|
+
return path.join(
|
|
3881
|
+
deploymentId ? path.join(base, String(deploymentId)) : base,
|
|
2882
3882
|
'staged-resources.json'
|
|
2883
3883
|
);
|
|
2884
3884
|
}
|
|
2885
3885
|
|
|
2886
|
-
function stagedResourcesContextFileForChange(changeId) {
|
|
2887
|
-
return stagedResourcesFileForChange(changeId).replace(
|
|
3886
|
+
function stagedResourcesContextFileForChange(changeId, deploymentId) {
|
|
3887
|
+
return stagedResourcesFileForChange(changeId, deploymentId).replace(
|
|
2888
3888
|
/staged-resources\.json$/,
|
|
2889
3889
|
'staged-resources.context.json'
|
|
2890
3890
|
);
|
|
@@ -2902,14 +3902,23 @@ function currentStagedResourcesContext(target, changeId) {
|
|
|
2902
3902
|
appType: target.appType,
|
|
2903
3903
|
profile: target.profileName,
|
|
2904
3904
|
changeId,
|
|
3905
|
+
deploymentId: target.deploymentId || null,
|
|
2905
3906
|
baselineId: baseline.baselineId,
|
|
2906
3907
|
clientSessionId: baseline.clientSessionId || null,
|
|
2907
3908
|
};
|
|
2908
3909
|
}
|
|
2909
3910
|
|
|
2910
|
-
function writeStagedResourcesFiles(
|
|
2911
|
-
|
|
2912
|
-
|
|
3911
|
+
function writeStagedResourcesFiles(
|
|
3912
|
+
changeId,
|
|
3913
|
+
resources,
|
|
3914
|
+
context,
|
|
3915
|
+
deploymentId
|
|
3916
|
+
) {
|
|
3917
|
+
const file = stagedResourcesFileForChange(changeId, deploymentId);
|
|
3918
|
+
const contextFile = stagedResourcesContextFileForChange(
|
|
3919
|
+
changeId,
|
|
3920
|
+
deploymentId
|
|
3921
|
+
);
|
|
2913
3922
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2914
3923
|
for (const [targetFile, value] of [
|
|
2915
3924
|
[file, resources],
|
|
@@ -2926,17 +3935,29 @@ function writeStagedResourcesFiles(changeId, resources, context) {
|
|
|
2926
3935
|
|
|
2927
3936
|
function initializeStagedAppReleaseResources(target, changeId) {
|
|
2928
3937
|
const context = currentStagedResourcesContext(target, changeId);
|
|
2929
|
-
return writeStagedResourcesFiles(
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3938
|
+
return writeStagedResourcesFiles(
|
|
3939
|
+
changeId,
|
|
3940
|
+
[],
|
|
3941
|
+
{
|
|
3942
|
+
...context,
|
|
3943
|
+
initializedAt: new Date().toISOString(),
|
|
3944
|
+
},
|
|
3945
|
+
target.deploymentId
|
|
3946
|
+
);
|
|
2933
3947
|
}
|
|
2934
3948
|
|
|
2935
3949
|
function assertStagedResourcesContext(target, changeId) {
|
|
2936
3950
|
const expected = currentStagedResourcesContext(target, changeId);
|
|
2937
|
-
const contextFile = stagedResourcesContextFileForChange(
|
|
3951
|
+
const contextFile = stagedResourcesContextFileForChange(
|
|
3952
|
+
changeId,
|
|
3953
|
+
target.deploymentId
|
|
3954
|
+
);
|
|
2938
3955
|
if (!fs.existsSync(contextFile)) {
|
|
2939
|
-
if (
|
|
3956
|
+
if (
|
|
3957
|
+
!fs.existsSync(
|
|
3958
|
+
stagedResourcesFileForChange(changeId, target.deploymentId)
|
|
3959
|
+
)
|
|
3960
|
+
) {
|
|
2940
3961
|
initializeStagedAppReleaseResources(target, changeId);
|
|
2941
3962
|
}
|
|
2942
3963
|
}
|
|
@@ -2951,6 +3972,7 @@ function assertStagedResourcesContext(target, changeId) {
|
|
|
2951
3972
|
'appType',
|
|
2952
3973
|
'profile',
|
|
2953
3974
|
'changeId',
|
|
3975
|
+
'deploymentId',
|
|
2954
3976
|
'baselineId',
|
|
2955
3977
|
'clientSessionId',
|
|
2956
3978
|
]) {
|
|
@@ -2971,7 +3993,7 @@ function recordStagedAppReleaseResources(
|
|
|
2971
3993
|
if (!changeId || !Array.isArray(resources) || resources.length === 0) {
|
|
2972
3994
|
return null;
|
|
2973
3995
|
}
|
|
2974
|
-
const file = stagedResourcesFileForChange(changeId);
|
|
3996
|
+
const file = stagedResourcesFileForChange(changeId, target.deploymentId);
|
|
2975
3997
|
const context = assertStagedResourcesContext(target, changeId);
|
|
2976
3998
|
let existing = [];
|
|
2977
3999
|
if (fs.existsSync(file)) {
|
|
@@ -2991,7 +4013,12 @@ function recordStagedAppReleaseResources(
|
|
|
2991
4013
|
const rightKey = describeAppReleaseResource(right, 0, 'stagedResources').stableKey;
|
|
2992
4014
|
return leftKey.localeCompare(rightKey);
|
|
2993
4015
|
});
|
|
2994
|
-
return writeStagedResourcesFiles(
|
|
4016
|
+
return writeStagedResourcesFiles(
|
|
4017
|
+
changeId,
|
|
4018
|
+
output,
|
|
4019
|
+
context,
|
|
4020
|
+
target.deploymentId
|
|
4021
|
+
);
|
|
2995
4022
|
}
|
|
2996
4023
|
|
|
2997
4024
|
function normalizeSatisfiedAppReleaseSelectors(values) {
|
|
@@ -3005,7 +4032,10 @@ function normalizeSatisfiedAppReleaseSelectors(values) {
|
|
|
3005
4032
|
function recordSatisfiedAppReleaseSelectors(changeId, selectors, target) {
|
|
3006
4033
|
const incoming = normalizeSatisfiedAppReleaseSelectors(selectors);
|
|
3007
4034
|
if (!changeId || incoming.length === 0) return [];
|
|
3008
|
-
const contextFile = stagedResourcesContextFileForChange(
|
|
4035
|
+
const contextFile = stagedResourcesContextFileForChange(
|
|
4036
|
+
changeId,
|
|
4037
|
+
target.deploymentId
|
|
4038
|
+
);
|
|
3009
4039
|
const context = assertStagedResourcesContext(target, changeId);
|
|
3010
4040
|
const satisfiedSelectors = normalizeSatisfiedAppReleaseSelectors([
|
|
3011
4041
|
...(context.satisfiedSelectors || []),
|
|
@@ -3353,12 +4383,21 @@ async function activateAppReleaseDetail(
|
|
|
3353
4383
|
? { activateStagedChildren: true }
|
|
3354
4384
|
: {}),
|
|
3355
4385
|
protocolVersion: options.protocolVersion,
|
|
4386
|
+
...(options.environmentControl || {}),
|
|
3356
4387
|
})
|
|
3357
4388
|
),
|
|
3358
4389
|
}
|
|
3359
4390
|
);
|
|
3360
4391
|
}
|
|
3361
4392
|
|
|
4393
|
+
function appReleaseEnvironmentControl(flags = {}) {
|
|
4394
|
+
return stripUndefinedValues({
|
|
4395
|
+
environmentId: readStringFlag(flags, 'environment-id') || undefined,
|
|
4396
|
+
candidateId: readStringFlag(flags, 'candidate-id') || undefined,
|
|
4397
|
+
deploymentId: readStringFlag(flags, 'deployment-id') || undefined,
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
|
|
3362
4401
|
function summarizeAppReleaseHead(data) {
|
|
3363
4402
|
const value =
|
|
3364
4403
|
data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
@@ -3611,6 +4650,7 @@ async function runAppReleaseCommand(
|
|
|
3611
4650
|
activateStagedChildren:
|
|
3612
4651
|
activatesStagedChildren,
|
|
3613
4652
|
protocolVersion: protocolVersion || undefined,
|
|
4653
|
+
environmentControl: appReleaseEnvironmentControl(flags),
|
|
3614
4654
|
}
|
|
3615
4655
|
);
|
|
3616
4656
|
} else if (subcommand === 'app-finalize') {
|
|
@@ -3685,6 +4725,7 @@ async function runAppReleaseCommand(
|
|
|
3685
4725
|
{
|
|
3686
4726
|
activateStagedChildren: Boolean(stagedResources),
|
|
3687
4727
|
protocolVersion,
|
|
4728
|
+
environmentControl: appReleaseEnvironmentControl(flags),
|
|
3688
4729
|
}
|
|
3689
4730
|
);
|
|
3690
4731
|
const healthWait = flags.wait
|
|
@@ -12027,7 +13068,9 @@ async function commands(args) {
|
|
|
12027
13068
|
'open-api credential list|get|create|update|rotate-secret [id]',
|
|
12028
13069
|
'design gates|template|review [--topic code]',
|
|
12029
13070
|
'sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive',
|
|
12030
|
-
'
|
|
13071
|
+
'environment init|attach|bind|list|status|diff|use',
|
|
13072
|
+
'release candidate|deploy|test|promote|rollback|publish|begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
|
|
13073
|
+
'studio [--no-open] [--port 0]',
|
|
12031
13074
|
'env',
|
|
12032
13075
|
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
12033
13076
|
'app list|create|snapshot',
|
|
@@ -12221,8 +13264,40 @@ function printSkillBootstrapReport(result) {
|
|
|
12221
13264
|
}
|
|
12222
13265
|
|
|
12223
13266
|
function getWorkspaceTarget(config, profileName, flags = {}) {
|
|
12224
|
-
const resolved = getProfile(config, profileName);
|
|
12225
13267
|
const state = loadProjectState();
|
|
13268
|
+
const environmentSelector =
|
|
13269
|
+
flags.environment || flags.target || state.currentTarget;
|
|
13270
|
+
if (
|
|
13271
|
+
environmentSelector &&
|
|
13272
|
+
state.logicalApp &&
|
|
13273
|
+
Object.keys(state.targets || {}).length > 0
|
|
13274
|
+
) {
|
|
13275
|
+
const selected = resolveEnvironmentTarget(state, environmentSelector);
|
|
13276
|
+
const targetProfileName = selected.binding.profile;
|
|
13277
|
+
if (profileName && profileName !== targetProfileName) {
|
|
13278
|
+
fail(
|
|
13279
|
+
`target ${selected.targetName} 固定使用 profile ${targetProfileName},不能改写为 ${profileName}`
|
|
13280
|
+
);
|
|
13281
|
+
}
|
|
13282
|
+
const resolved = getProfile(config, targetProfileName);
|
|
13283
|
+
if (!selected.binding.appType || !selected.binding.environmentId) {
|
|
13284
|
+
fail(`target ${selected.targetName} 缺少 appType 或 environmentId`);
|
|
13285
|
+
}
|
|
13286
|
+
ensureResourceBuckets(selected.binding);
|
|
13287
|
+
return {
|
|
13288
|
+
profileName: resolved.profileName,
|
|
13289
|
+
profile: resolved.profile,
|
|
13290
|
+
state,
|
|
13291
|
+
bound: selected.binding,
|
|
13292
|
+
appType: selected.binding.appType,
|
|
13293
|
+
targetName: selected.targetName,
|
|
13294
|
+
environmentId: selected.binding.environmentId,
|
|
13295
|
+
environmentKind: selected.binding.kind,
|
|
13296
|
+
deploymentId: readStringFlag(flags, 'deployment-id') || null,
|
|
13297
|
+
logicalApp: state.logicalApp,
|
|
13298
|
+
};
|
|
13299
|
+
}
|
|
13300
|
+
const resolved = getProfile(config, profileName);
|
|
12226
13301
|
const bound = state.profiles?.[resolved.profileName] || {};
|
|
12227
13302
|
const appType = flags['app-type'] || bound.appType;
|
|
12228
13303
|
if (!appType) {
|
|
@@ -13130,6 +14205,12 @@ function saveNotificationTypeConfigResource(target, configCode, configId, extra
|
|
|
13130
14205
|
}
|
|
13131
14206
|
|
|
13132
14207
|
function prepareStateResourceWrite(target) {
|
|
14208
|
+
const managedBinding = resolveManagedStateBinding(target);
|
|
14209
|
+
if (managedBinding) {
|
|
14210
|
+
ensureResourceBuckets(managedBinding);
|
|
14211
|
+
target.bound = managedBinding;
|
|
14212
|
+
return managedBinding;
|
|
14213
|
+
}
|
|
13133
14214
|
target.state.profiles = target.state.profiles || {};
|
|
13134
14215
|
target.state.profiles[target.profileName] = {
|
|
13135
14216
|
...target.bound,
|
|
@@ -13156,15 +14237,7 @@ function saveFormPermissionGroupResource(target, groupCode, groupId, extra = {})
|
|
|
13156
14237
|
}
|
|
13157
14238
|
|
|
13158
14239
|
function saveStateResource(target, bucket, code, value = {}, keys = Object.keys(value)) {
|
|
13159
|
-
|
|
13160
|
-
target.state.profiles[target.profileName] = {
|
|
13161
|
-
...target.bound,
|
|
13162
|
-
baseUrl: target.profile.baseUrl,
|
|
13163
|
-
appType: target.appType,
|
|
13164
|
-
updatedAt: new Date().toISOString(),
|
|
13165
|
-
};
|
|
13166
|
-
const nextBound = target.state.profiles[target.profileName];
|
|
13167
|
-
ensureResourceBuckets(nextBound);
|
|
14240
|
+
const nextBound = prepareStateResourceWrite(target);
|
|
13168
14241
|
const previous = nextBound.resources[bucket]?.[code] || {};
|
|
13169
14242
|
nextBound.resources[bucket] = nextBound.resources[bucket] || {};
|
|
13170
14243
|
nextBound.resources[bucket][code] = {
|