openxiangda 1.0.208 → 1.0.209
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 +2 -2
- package/lib/application-environments.js +73 -0
- package/lib/cli.js +240 -61
- package/openxiangda-skills/SKILL.md +2 -2
- package/openxiangda-skills/references/workspace-state.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +4 -2
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +2 -0
- package/templates/openxiangda-react-spa/AGENTS.md +1 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +2 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +1 -1
package/README.md
CHANGED
|
@@ -80,9 +80,9 @@ openxiangda inspect app --profile dev --json
|
|
|
80
80
|
openxiangda app snapshot APP_XXXX --profile dev --json
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only profile-specific resource IDs; CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
|
|
83
|
+
User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only durable profile-specific resource IDs and environment bindings; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal so a release does not dirty the reviewed Git state. Durable CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
|
|
84
84
|
|
|
85
|
-
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` is deliberately two-phase: the first invocation seals the clean authoritative Git revision and deploys one immutable candidate only to preproduction; it always stops at `awaiting_production_confirmation`. A later invocation includes `--confirm-production` and promotes that exact candidate to production without rebuilding it. Real human acceptance is the recommended default; an optional `--acceptance-note` records it in the deployment evidence, but the platform does not force every low-risk or emergency release through a rigid approval gate. It is impossible to prepare and promote in one invocation. For an audited catch-up whose exact non-delete targets are already merged but whose active resources came from several historical release lineages, the first invocation may explicitly add `--adopt-online-baseline --adoption-reason "..."`; ship validates this pair before creating a candidate/deployment, then forwards it only to exact scoped resource stages. A reviewed complete Function/Automation manifest replacement may similarly add the inseparable `--replace-manifest --reason "..."` pair; ship forwards it only to an exact Backend stage and requires the production confirmation to repeat the exact preproduction intent. Frozen online heads, change/lease ownership, server CAS, staged children, and the single App finalize remain mandatory. Supported configuration resources use exact `resourceSelectors`; a legacy `resources=true` category marker is narrowed by those selectors, while unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. The lower-level `candidate/deploy/test/promote` commands remain available for recovery and diagnostics. Direct `release publish` is retained only for legacy, unmanaged workspaces and fails closed for either target of an environment-managed application.
|
|
85
|
+
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` is deliberately two-phase: the first invocation seals the clean authoritative Git revision and deploys one immutable candidate only to preproduction; it always stops at `awaiting_production_confirmation`. A later invocation includes `--confirm-production` and promotes that exact candidate to production without rebuilding it. Real human acceptance is the recommended default; an optional `--acceptance-note` records it in the deployment evidence, but the platform does not force every low-risk or emergency release through a rigid approval gate. It is impossible to prepare and promote in one invocation. For an audited catch-up whose exact non-delete targets are already merged but whose active resources came from several historical release lineages, the first invocation may explicitly add `--adopt-online-baseline --adoption-reason "..."`; ship validates and freezes this pair before creating a candidate/deployment, then automatically reuses it during the later production confirmation and forwards it only to exact scoped resource stages. A reviewed complete Function/Automation manifest replacement may similarly add the inseparable `--replace-manifest --reason "..."` pair; ship forwards it only to an exact Backend stage and requires the production confirmation to repeat the exact preproduction intent. Frozen online heads, change/lease ownership, server CAS, staged children, and the single App finalize remain mandatory. Supported configuration resources use exact `resourceSelectors`; a legacy `resources=true` category marker is narrowed by those selectors, while unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. The lower-level `candidate/deploy/test/promote` commands remain available for recovery and diagnostics. Direct `release publish` is retained only for legacy, unmanaged workspaces and fails closed for either target of an environment-managed application.
|
|
86
86
|
|
|
87
87
|
Exact, non-destructive configuration selectors such as Data Views and permission groups are now sequenced automatically inside the same ship journal instead of requiring separate SDD changes. New forms are idempotently ensured per environment before their immutable FormRelease is staged. Unscoped resources and destructive configuration deletes remain fail-closed.
|
|
88
88
|
|
|
@@ -474,6 +474,76 @@ function readCandidate(candidateId, cwd = process.cwd()) {
|
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
+
function managedTargetDeploymentFile(target, cwd = process.cwd()) {
|
|
478
|
+
const safeSegment = (value, fallback) =>
|
|
479
|
+
String(value || fallback)
|
|
480
|
+
.trim()
|
|
481
|
+
.replace(/[^A-Za-z0-9._-]+/g, '_')
|
|
482
|
+
.slice(0, 128);
|
|
483
|
+
return path.join(
|
|
484
|
+
cwd,
|
|
485
|
+
'.openxiangda',
|
|
486
|
+
'releases',
|
|
487
|
+
'targets',
|
|
488
|
+
safeSegment(target.logicalApp?.code, 'unmanaged'),
|
|
489
|
+
`${safeSegment(
|
|
490
|
+
target.targetName || target.environmentId || target.appType,
|
|
491
|
+
'default'
|
|
492
|
+
)}.json`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function readRememberedTargetDeployment(target, cwd = process.cwd()) {
|
|
497
|
+
const file = managedTargetDeploymentFile(target, cwd);
|
|
498
|
+
if (fs.existsSync(file)) {
|
|
499
|
+
try {
|
|
500
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`MANAGED_TARGET_DEPLOYMENT_STATE_INVALID: ${path.relative(
|
|
504
|
+
cwd,
|
|
505
|
+
file
|
|
506
|
+
)} 无法读取: ${error.message}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (
|
|
511
|
+
target.bound?.lastDeploymentId ||
|
|
512
|
+
target.bound?.lastCandidateId ||
|
|
513
|
+
target.bound?.lastDeploymentStatus
|
|
514
|
+
) {
|
|
515
|
+
return {
|
|
516
|
+
schemaVersion: 'openxiangda_target_deployment_v1',
|
|
517
|
+
deploymentId: target.bound.lastDeploymentId || null,
|
|
518
|
+
candidateId: target.bound.lastCandidateId || null,
|
|
519
|
+
status: target.bound.lastDeploymentStatus || null,
|
|
520
|
+
migratedFromProjectState: true,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function rememberTargetDeployment(
|
|
527
|
+
target,
|
|
528
|
+
deployment,
|
|
529
|
+
cwd = process.cwd()
|
|
530
|
+
) {
|
|
531
|
+
const record = {
|
|
532
|
+
schemaVersion: 'openxiangda_target_deployment_v1',
|
|
533
|
+
logicalAppCode: target.logicalApp?.code || null,
|
|
534
|
+
targetName: target.targetName || null,
|
|
535
|
+
environmentId: target.environmentId || null,
|
|
536
|
+
appType: target.appType,
|
|
537
|
+
deploymentId: deployment.id,
|
|
538
|
+
candidateId: deployment.candidateId || null,
|
|
539
|
+
status: deployment.status,
|
|
540
|
+
updatedAt: new Date().toISOString(),
|
|
541
|
+
};
|
|
542
|
+
const file = managedTargetDeploymentFile(target, cwd);
|
|
543
|
+
writePrivateJsonAtomic(file, record);
|
|
544
|
+
return { record, file };
|
|
545
|
+
}
|
|
546
|
+
|
|
477
547
|
function readJsonInput(value, label, cwd = process.cwd()) {
|
|
478
548
|
const input = String(value || '').trim();
|
|
479
549
|
if (!input) throw new Error(`${label} 不能为空`);
|
|
@@ -648,9 +718,12 @@ module.exports = {
|
|
|
648
718
|
normalizeSideEffectPolicyInput,
|
|
649
719
|
normalizeTargetName,
|
|
650
720
|
hasStateResourceMappings,
|
|
721
|
+
managedTargetDeploymentFile,
|
|
651
722
|
readCandidate,
|
|
652
723
|
readJsonInput,
|
|
724
|
+
readRememberedTargetDeployment,
|
|
653
725
|
releaseExecutionPath,
|
|
726
|
+
rememberTargetDeployment,
|
|
654
727
|
resolveManagedStateBinding,
|
|
655
728
|
resolveEnvironmentTarget,
|
|
656
729
|
saveCandidate,
|
package/lib/cli.js
CHANGED
|
@@ -139,7 +139,9 @@ const {
|
|
|
139
139
|
normalizeReleaseSourceRevisionForBaseline,
|
|
140
140
|
readCandidate,
|
|
141
141
|
readJsonInput,
|
|
142
|
+
readRememberedTargetDeployment,
|
|
142
143
|
releaseExecutionPath,
|
|
144
|
+
rememberTargetDeployment,
|
|
143
145
|
resolveManagedStateBinding,
|
|
144
146
|
resolveEnvironmentTarget,
|
|
145
147
|
saveCandidate,
|
|
@@ -1104,6 +1106,128 @@ function writeManagedShip(changeId, value) {
|
|
|
1104
1106
|
};
|
|
1105
1107
|
}
|
|
1106
1108
|
|
|
1109
|
+
function normalizeManagedShipIntent(value = {}) {
|
|
1110
|
+
return {
|
|
1111
|
+
enabled: Boolean(value.enabled),
|
|
1112
|
+
reason: String(value.reason || '').trim() || null,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function managedShipIntentMatches(left, right) {
|
|
1117
|
+
const normalizedLeft = normalizeManagedShipIntent(left);
|
|
1118
|
+
const normalizedRight = normalizeManagedShipIntent(right);
|
|
1119
|
+
return (
|
|
1120
|
+
normalizedLeft.enabled === normalizedRight.enabled &&
|
|
1121
|
+
normalizedLeft.reason === normalizedRight.reason
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function resolveManagedShipIntents(flags = {}, existingShip = null) {
|
|
1126
|
+
const manifestReplacement = normalizeManagedShipIntent({
|
|
1127
|
+
enabled: flags['replace-manifest'],
|
|
1128
|
+
reason: flags.reason,
|
|
1129
|
+
});
|
|
1130
|
+
const requestedBaselineAdoption = normalizeManagedShipIntent({
|
|
1131
|
+
enabled: flags['adopt-online-baseline'],
|
|
1132
|
+
reason: flags['adoption-reason'],
|
|
1133
|
+
});
|
|
1134
|
+
const hasFrozenCandidate = Boolean(existingShip?.candidateId);
|
|
1135
|
+
let baselineAdoption = requestedBaselineAdoption;
|
|
1136
|
+
|
|
1137
|
+
if (hasFrozenCandidate) {
|
|
1138
|
+
const recordedReplacement = normalizeManagedShipIntent(
|
|
1139
|
+
existingShip.manifestReplacement
|
|
1140
|
+
);
|
|
1141
|
+
if (!managedShipIntentMatches(recordedReplacement, manifestReplacement)) {
|
|
1142
|
+
fail(
|
|
1143
|
+
'RELEASE_SHIP_REPLACE_MANIFEST_INTENT_MISMATCH: --replace-manifest/--reason 必须与该 candidate 的预发发布意图完全一致;请使用 ship.json 记录的同一对参数'
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
const hasRecordedAdoption = Object.prototype.hasOwnProperty.call(
|
|
1148
|
+
existingShip,
|
|
1149
|
+
'baselineAdoption'
|
|
1150
|
+
);
|
|
1151
|
+
const recordedAdoption = normalizeManagedShipIntent(
|
|
1152
|
+
existingShip.baselineAdoption
|
|
1153
|
+
);
|
|
1154
|
+
const adoptionExplicitlySupplied =
|
|
1155
|
+
Object.prototype.hasOwnProperty.call(
|
|
1156
|
+
flags,
|
|
1157
|
+
'adopt-online-baseline'
|
|
1158
|
+
) ||
|
|
1159
|
+
Object.prototype.hasOwnProperty.call(flags, 'adoption-reason');
|
|
1160
|
+
if (
|
|
1161
|
+
hasRecordedAdoption &&
|
|
1162
|
+
adoptionExplicitlySupplied &&
|
|
1163
|
+
!managedShipIntentMatches(recordedAdoption, requestedBaselineAdoption)
|
|
1164
|
+
) {
|
|
1165
|
+
fail(
|
|
1166
|
+
'RELEASE_SHIP_ADOPTION_INTENT_MISMATCH: --adopt-online-baseline/--adoption-reason 与该 candidate 的预发发布意图不一致;省略这两个参数可自动复用 ship.json 中的已冻结意图'
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
baselineAdoption = adoptionExplicitlySupplied
|
|
1170
|
+
? requestedBaselineAdoption
|
|
1171
|
+
: recordedAdoption;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const effectiveFlags = { ...flags };
|
|
1175
|
+
if (baselineAdoption.enabled) {
|
|
1176
|
+
effectiveFlags['adopt-online-baseline'] = true;
|
|
1177
|
+
effectiveFlags['adoption-reason'] = baselineAdoption.reason;
|
|
1178
|
+
} else {
|
|
1179
|
+
delete effectiveFlags['adopt-online-baseline'];
|
|
1180
|
+
delete effectiveFlags['adoption-reason'];
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
manifestReplacement,
|
|
1185
|
+
baselineAdoption,
|
|
1186
|
+
effectiveFlags,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function buildManagedShipCommand(
|
|
1191
|
+
changeId,
|
|
1192
|
+
profileName,
|
|
1193
|
+
intents,
|
|
1194
|
+
options = {}
|
|
1195
|
+
) {
|
|
1196
|
+
const args = [
|
|
1197
|
+
'openxiangda release ship',
|
|
1198
|
+
'--change',
|
|
1199
|
+
JSON.stringify(changeId),
|
|
1200
|
+
'--profile',
|
|
1201
|
+
JSON.stringify(profileName),
|
|
1202
|
+
];
|
|
1203
|
+
if (options.confirmProduction) args.push('--confirm-production');
|
|
1204
|
+
if (intents.manifestReplacement.enabled) {
|
|
1205
|
+
args.push(
|
|
1206
|
+
'--replace-manifest',
|
|
1207
|
+
'--reason',
|
|
1208
|
+
JSON.stringify(intents.manifestReplacement.reason)
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (intents.baselineAdoption.enabled) {
|
|
1212
|
+
args.push(
|
|
1213
|
+
'--adopt-online-baseline',
|
|
1214
|
+
'--adoption-reason',
|
|
1215
|
+
JSON.stringify(intents.baselineAdoption.reason)
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
return args.join(' ');
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function managedShipFailure(error) {
|
|
1222
|
+
return {
|
|
1223
|
+
code: String(error?.code || 'OPENXIANGDA_DEPLOYMENT_FAILED'),
|
|
1224
|
+
message: maskText(error?.message || String(error)).slice(0, 2000),
|
|
1225
|
+
deploymentId: error?.deploymentId || null,
|
|
1226
|
+
deploymentStatus: error?.deploymentStatus || 'failed',
|
|
1227
|
+
recordedAt: new Date().toISOString(),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1107
1231
|
function writePrivateJsonAtomic(file, value) {
|
|
1108
1232
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1109
1233
|
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -2717,14 +2841,6 @@ function applicationDeploymentApiPath(logicalAppCode, deploymentId, action) {
|
|
|
2717
2841
|
return environmentSetApiPath(logicalAppCode, suffix);
|
|
2718
2842
|
}
|
|
2719
2843
|
|
|
2720
|
-
function rememberTargetDeployment(target, deployment) {
|
|
2721
|
-
target.bound.lastDeploymentId = deployment.id;
|
|
2722
|
-
target.bound.lastCandidateId = deployment.candidateId || null;
|
|
2723
|
-
target.bound.lastDeploymentStatus = deployment.status;
|
|
2724
|
-
target.bound.updatedAt = new Date().toISOString();
|
|
2725
|
-
saveProjectState(target.state);
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
2844
|
function assertLocalCandidate(candidate) {
|
|
2729
2845
|
if (
|
|
2730
2846
|
!candidate?.id ||
|
|
@@ -2906,7 +3022,7 @@ async function markApplicationDeploymentFailed(
|
|
|
2906
3022
|
error
|
|
2907
3023
|
) {
|
|
2908
3024
|
try {
|
|
2909
|
-
await requestWithAuth(
|
|
3025
|
+
return await requestWithAuth(
|
|
2910
3026
|
config,
|
|
2911
3027
|
target.profileName,
|
|
2912
3028
|
applicationDeploymentApiPath(
|
|
@@ -2930,6 +3046,7 @@ async function markApplicationDeploymentFailed(
|
|
|
2930
3046
|
warn(
|
|
2931
3047
|
`部署失败记录未写入平台: ${maskText(recordError?.message || recordError)}`
|
|
2932
3048
|
);
|
|
3049
|
+
return null;
|
|
2933
3050
|
}
|
|
2934
3051
|
}
|
|
2935
3052
|
|
|
@@ -3003,13 +3120,24 @@ async function deployApplicationCandidate(
|
|
|
3003
3120
|
},
|
|
3004
3121
|
};
|
|
3005
3122
|
}
|
|
3006
|
-
await markApplicationDeploymentFailed(
|
|
3123
|
+
const failedDeployment = await markApplicationDeploymentFailed(
|
|
3007
3124
|
config,
|
|
3008
3125
|
target,
|
|
3009
3126
|
logicalApp.code,
|
|
3010
3127
|
deployment.id,
|
|
3011
3128
|
error
|
|
3012
3129
|
);
|
|
3130
|
+
rememberTargetDeployment(
|
|
3131
|
+
target,
|
|
3132
|
+
failedDeployment?.id
|
|
3133
|
+
? failedDeployment
|
|
3134
|
+
: {
|
|
3135
|
+
...deployment,
|
|
3136
|
+
status: 'failed',
|
|
3137
|
+
}
|
|
3138
|
+
);
|
|
3139
|
+
error.deploymentId = deployment.id;
|
|
3140
|
+
error.deploymentStatus = failedDeployment?.status || 'failed';
|
|
3013
3141
|
throw error;
|
|
3014
3142
|
}
|
|
3015
3143
|
}
|
|
@@ -3173,26 +3301,12 @@ async function runApplicationShipCommand(
|
|
|
3173
3301
|
);
|
|
3174
3302
|
const existingShip = readManagedShip(changeId);
|
|
3175
3303
|
const confirmProduction = Boolean(flags['confirm-production']);
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
const recordedReplacement = existingShip.manifestReplacement || {
|
|
3183
|
-
enabled: false,
|
|
3184
|
-
reason: null,
|
|
3185
|
-
};
|
|
3186
|
-
if (
|
|
3187
|
-
Boolean(recordedReplacement.enabled) !== manifestReplacement.enabled ||
|
|
3188
|
-
String(recordedReplacement.reason || '') !==
|
|
3189
|
-
String(manifestReplacement.reason || '')
|
|
3190
|
-
) {
|
|
3191
|
-
fail(
|
|
3192
|
-
'RELEASE_SHIP_REPLACE_MANIFEST_INTENT_MISMATCH: --replace-manifest/--reason 必须与该 candidate 的预发发布意图完全一致;请使用 ship.json 记录的同一对参数'
|
|
3193
|
-
);
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3304
|
+
const intents = resolveManagedShipIntents(flags, existingShip);
|
|
3305
|
+
const {
|
|
3306
|
+
manifestReplacement,
|
|
3307
|
+
baselineAdoption,
|
|
3308
|
+
effectiveFlags,
|
|
3309
|
+
} = intents;
|
|
3196
3310
|
|
|
3197
3311
|
if (confirmProduction && !existingShip?.preproductionDeploymentId) {
|
|
3198
3312
|
fail(
|
|
@@ -3209,7 +3323,7 @@ async function runApplicationShipCommand(
|
|
|
3209
3323
|
config,
|
|
3210
3324
|
preproduction,
|
|
3211
3325
|
{
|
|
3212
|
-
...
|
|
3326
|
+
...effectiveFlags,
|
|
3213
3327
|
change: changeId,
|
|
3214
3328
|
'test-plan-json': JSON.stringify(
|
|
3215
3329
|
managedShipTestPlan()
|
|
@@ -3225,6 +3339,7 @@ async function runApplicationShipCommand(
|
|
|
3225
3339
|
sourceRevision: candidate.sourceRevision,
|
|
3226
3340
|
candidateFile: created.candidateFile,
|
|
3227
3341
|
manifestReplacement,
|
|
3342
|
+
baselineAdoption,
|
|
3228
3343
|
});
|
|
3229
3344
|
} else {
|
|
3230
3345
|
assertLocalCandidate(candidate);
|
|
@@ -3243,13 +3358,34 @@ async function runApplicationShipCommand(
|
|
|
3243
3358
|
!deployment ||
|
|
3244
3359
|
!['deployed', 'succeeded'].includes(deployment.status)
|
|
3245
3360
|
) {
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3361
|
+
try {
|
|
3362
|
+
const deployed = await deployApplicationCandidate(
|
|
3363
|
+
config,
|
|
3364
|
+
preproduction,
|
|
3365
|
+
candidate,
|
|
3366
|
+
{ kind: 'deploy', flags: effectiveFlags }
|
|
3367
|
+
);
|
|
3368
|
+
deployment = deployed.deployment;
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
const failure = managedShipFailure(error);
|
|
3371
|
+
writeManagedShip(changeId, {
|
|
3372
|
+
status: 'preproduction_failed',
|
|
3373
|
+
candidateId: candidate.id,
|
|
3374
|
+
candidateHash: candidate.candidateHash,
|
|
3375
|
+
preproductionEnvironment: preproduction.targetName,
|
|
3376
|
+
preproductionDeploymentId: failure.deploymentId,
|
|
3377
|
+
preproductionStatus: failure.deploymentStatus,
|
|
3378
|
+
manifestReplacement,
|
|
3379
|
+
baselineAdoption,
|
|
3380
|
+
failure,
|
|
3381
|
+
nextCommand: buildManagedShipCommand(
|
|
3382
|
+
changeId,
|
|
3383
|
+
preproduction.profileName,
|
|
3384
|
+
intents
|
|
3385
|
+
),
|
|
3386
|
+
});
|
|
3387
|
+
throw error;
|
|
3388
|
+
}
|
|
3253
3389
|
}
|
|
3254
3390
|
const saved = writeManagedShip(changeId, {
|
|
3255
3391
|
status: 'awaiting_production_confirmation',
|
|
@@ -3262,17 +3398,17 @@ async function runApplicationShipCommand(
|
|
|
3262
3398
|
deployment.targetAppReleaseId || null,
|
|
3263
3399
|
preproductionStatus: deployment.status,
|
|
3264
3400
|
manifestReplacement,
|
|
3401
|
+
baselineAdoption,
|
|
3402
|
+
failure: null,
|
|
3265
3403
|
preparedAt:
|
|
3266
3404
|
existingShip?.preparedAt ||
|
|
3267
3405
|
new Date().toISOString(),
|
|
3268
|
-
nextCommand:
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
)}`
|
|
3275
|
-
: ''),
|
|
3406
|
+
nextCommand: buildManagedShipCommand(
|
|
3407
|
+
changeId,
|
|
3408
|
+
preproduction.profileName,
|
|
3409
|
+
intents,
|
|
3410
|
+
{ confirmProduction: true }
|
|
3411
|
+
),
|
|
3276
3412
|
});
|
|
3277
3413
|
return {
|
|
3278
3414
|
phase: 'preproduction',
|
|
@@ -3394,17 +3530,38 @@ async function runApplicationShipCommand(
|
|
|
3394
3530
|
manualAcceptance,
|
|
3395
3531
|
preproductionStatus: preproductionDeployment.status,
|
|
3396
3532
|
});
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3533
|
+
let promoted;
|
|
3534
|
+
try {
|
|
3535
|
+
promoted = await deployApplicationCandidate(
|
|
3536
|
+
config,
|
|
3537
|
+
production,
|
|
3538
|
+
candidate,
|
|
3539
|
+
{
|
|
3540
|
+
kind: 'promotion',
|
|
3541
|
+
sourceEnvironmentId:
|
|
3542
|
+
preproduction.environmentId,
|
|
3543
|
+
flags: effectiveFlags,
|
|
3544
|
+
}
|
|
3545
|
+
);
|
|
3546
|
+
} catch (error) {
|
|
3547
|
+
const failure = managedShipFailure(error);
|
|
3548
|
+
writeManagedShip(changeId, {
|
|
3549
|
+
status: 'production_failed',
|
|
3550
|
+
productionEnvironment: production.targetName,
|
|
3551
|
+
productionDeploymentId: failure.deploymentId,
|
|
3552
|
+
productionStatus: failure.deploymentStatus,
|
|
3553
|
+
manifestReplacement,
|
|
3554
|
+
baselineAdoption,
|
|
3555
|
+
failure,
|
|
3556
|
+
nextCommand: buildManagedShipCommand(
|
|
3557
|
+
changeId,
|
|
3558
|
+
preproduction.profileName,
|
|
3559
|
+
intents,
|
|
3560
|
+
{ confirmProduction: true }
|
|
3561
|
+
),
|
|
3562
|
+
});
|
|
3563
|
+
throw error;
|
|
3564
|
+
}
|
|
3408
3565
|
const saved = writeManagedShip(changeId, {
|
|
3409
3566
|
status: 'completed',
|
|
3410
3567
|
productionEnvironment: production.targetName,
|
|
@@ -3412,6 +3569,7 @@ async function runApplicationShipCommand(
|
|
|
3412
3569
|
productionAppReleaseId:
|
|
3413
3570
|
promoted.deployment.targetAppReleaseId || null,
|
|
3414
3571
|
productionStatus: promoted.deployment.status,
|
|
3572
|
+
failure: null,
|
|
3415
3573
|
completedAt: new Date().toISOString(),
|
|
3416
3574
|
});
|
|
3417
3575
|
return {
|
|
@@ -3458,7 +3616,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3458
3616
|
readStringFlag(flags, 'deployment') ||
|
|
3459
3617
|
readStringFlag(flags, 'deployment-id') ||
|
|
3460
3618
|
positional[0] ||
|
|
3461
|
-
target
|
|
3619
|
+
readRememberedTargetDeployment(target)?.deploymentId;
|
|
3462
3620
|
const reason = readStringFlag(flags, 'reason');
|
|
3463
3621
|
if (!deploymentId || reason.length < 8) {
|
|
3464
3622
|
fail(
|
|
@@ -3486,7 +3644,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3486
3644
|
readStringFlag(flags, 'candidate') ||
|
|
3487
3645
|
readStringFlag(flags, 'candidate-id') ||
|
|
3488
3646
|
positional[0] ||
|
|
3489
|
-
target
|
|
3647
|
+
readRememberedTargetDeployment(target)?.candidateId;
|
|
3490
3648
|
if (subcommand === 'deploy') {
|
|
3491
3649
|
if (target.environmentKind !== 'preproduction') {
|
|
3492
3650
|
fail('release deploy 只允许目标为 preproduction');
|
|
@@ -3527,7 +3685,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3527
3685
|
const deploymentId =
|
|
3528
3686
|
readStringFlag(flags, 'deployment') ||
|
|
3529
3687
|
readStringFlag(flags, 'deployment-id') ||
|
|
3530
|
-
target
|
|
3688
|
+
readRememberedTargetDeployment(target)?.deploymentId;
|
|
3531
3689
|
if (!deploymentId || !flags['evidence-json']) {
|
|
3532
3690
|
fail(
|
|
3533
3691
|
'release test 必须提供 --deployment <id>(首次可省略)和 --evidence-json <JSON|file>'
|
|
@@ -3636,7 +3794,7 @@ async function release(args) {
|
|
|
3636
3794
|
' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
|
|
3637
3795
|
'说明:',
|
|
3638
3796
|
' - ship 是环境托管应用的两段式快车道:首次只部署预发并停止;确认预发结果后,第二次以同一 candidate 晋级正式。人工验收备注可选。',
|
|
3639
|
-
' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason
|
|
3797
|
+
' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason;预发冻结后,正式确认会从 ship.json 自动复用同一意图,仍保留冻结 Head、CAS、租约、staged children 和单次 App finalize。',
|
|
3640
3798
|
' - ship 的 --replace-manifest 与 --reason 必须成对,只透传给精确 Backend Function/Automation stage;正式确认必须复用与预发完全相同的参数。',
|
|
3641
3799
|
' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
|
|
3642
3800
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
@@ -10719,6 +10877,27 @@ async function appFunction(args) {
|
|
|
10719
10877
|
if (subcommand === 'invoke') {
|
|
10720
10878
|
const [functionCode] = positional;
|
|
10721
10879
|
if (!functionCode) fail('用法: openxiangda function invoke <functionCode> [--body-json file|json]');
|
|
10880
|
+
if (
|
|
10881
|
+
target.logicalApp &&
|
|
10882
|
+
Object.keys(target.state.targets || {}).length > 1 &&
|
|
10883
|
+
!flags.environment &&
|
|
10884
|
+
!flags.target
|
|
10885
|
+
) {
|
|
10886
|
+
fail(
|
|
10887
|
+
'FUNCTION_INVOKE_ENVIRONMENT_REQUIRED: 托管应用调用 Function 必须显式提供 --environment <target>,避免把预发结果误判为生产结果'
|
|
10888
|
+
);
|
|
10889
|
+
}
|
|
10890
|
+
warn(
|
|
10891
|
+
[
|
|
10892
|
+
'Resolved target:',
|
|
10893
|
+
'operation=function invoke',
|
|
10894
|
+
`profile=${target.profileName}`,
|
|
10895
|
+
`environment=${target.targetName || 'legacy'}`,
|
|
10896
|
+
`kind=${target.environmentKind || 'unmanaged'}`,
|
|
10897
|
+
`appType=${target.appType}`,
|
|
10898
|
+
`environmentId=${target.environmentId || '-'}`,
|
|
10899
|
+
].join(' ')
|
|
10900
|
+
);
|
|
10722
10901
|
const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
|
|
10723
10902
|
const openXiangdaApiRequest = buildOpenXiangdaFunctionInvokeRequest(
|
|
10724
10903
|
target.appType,
|
|
@@ -106,7 +106,7 @@ openxiangda release ship --change <release-change> --profile <name> \
|
|
|
106
106
|
|
|
107
107
|
For a workspace registered by `openxiangda environment init` or connected to an existing set with `openxiangda environment attach`, production is never a direct publish target. Use the two-phase `release ship` fast path by default. Its first invocation creates/deploys the immutable candidate to preproduction and always returns `awaiting_production_confirmation`; it cannot promote. A second invocation with `--confirm-production` promotes the exact same candidate hash. Real human acceptance is the recommended normal practice and can be recorded with optional `--acceptance-note`, but do not invent an acceptance note and do not treat it as a universal hard platform gate when the user explicitly authorizes a low-risk or emergency promotion. The lower-level candidate/deploy/test/promote commands are recovery primitives, not the normal agent loop. Preproduction and production keep independent app/resource/data identities in `.openxiangda/state.json`; do not copy IDs between targets. `environment attach` may seed only the matching legacy app binding into preproduction and must leave production empty. Only during authorized commissioning/reclassification may `environment swap --reason "..." --confirm-production` atomically exchange the two existing roles; it preserves each app's data and Release Heads, remaps local resource IDs by appType, and does not enable production side effects by default. To change only one environment's side-effect policy, run `environment policy update <kind|id> --side-effect-policy-json <JSON|file> --reason "..." --dry-run` first, then repeat without `--dry-run`; never use swap for this. Policy reads are forward-compatible: patch mode validates only caller-supplied fields and preserves unrecognized historical fields, while `--full-replace` validates the supplied complete target and intentionally removes omissions. The update is revision-CAS, production needs `--confirm-production`, and `organizationWrites=explicit_capability_only` still requires `app:organization:manage`. Keep release hard gates limited to explicit scope/profile/target, authorization, clean pushed mainline, immutable identity, CAS/lease, and production confirmation; prose and human acceptance notes are advisory unless strict mode was intentionally configured. Use `openxiangda studio` for the loopback-only developer view of bindings, drift, evidence, and safe next actions.
|
|
108
108
|
|
|
109
|
-
For an audited catch-up whose exact non-delete targets are already merged but whose active resources combine multiple historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates the pair before candidate/deployment creation and forwards it only to exact scoped resource stages
|
|
109
|
+
For an audited catch-up whose exact non-delete targets are already merged but whose active resources combine multiple historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates and freezes the pair before candidate/deployment creation; the later `--confirm-production` invocation automatically reuses the same intent from the private ship journal and forwards it only to exact scoped resource stages. Frozen online heads, change/lease ownership, delete/prune/force rejection, server CAS, staged-child verification, and the single atomic App finalize remain mandatory.
|
|
110
110
|
|
|
111
111
|
When an environment-managed release intentionally replaces complete Function/Automation manifests, `release ship` may add the inseparable `--replace-manifest --reason "..."` pair. The reason must be at least 8 characters; only an exact Backend selector receives it, and the production confirmation must repeat the exact preproduction pair. It never widens Form, Workflow, Runtime, configuration, wildcard, or app-wide stages.
|
|
112
112
|
|
|
@@ -136,7 +136,7 @@ The lease is app-level promotion ownership, while worktree ownership prevents tw
|
|
|
136
136
|
|
|
137
137
|
## Always
|
|
138
138
|
|
|
139
|
-
- Treat `.openxiangda/state.json` as CLI-maintained ID mapping, not a task lock or merge mechanism.
|
|
139
|
+
- Treat `.openxiangda/state.json` as CLI-maintained durable ID/environment mapping, not a task lock or merge mechanism. Volatile candidate/deployment progress belongs under the private `.openxiangda/releases/` journal.
|
|
140
140
|
- Keep resource IDs isolated per profile; never copy IDs from dev to prod.
|
|
141
141
|
- Use platform roles, permission groups, public grants, and backend Function checks for authorization; frontend hiding is presentation only.
|
|
142
142
|
- Keep tokens and secrets out of project files and chat.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Workspace State
|
|
2
2
|
|
|
3
|
-
OpenXiangda project
|
|
3
|
+
OpenXiangda durable project bindings live in `.openxiangda/state.json`. Volatile release execution, candidate, deployment, and recovery progress lives under the private `.openxiangda/releases/` journal and must not be copied into the durable ID map.
|
|
4
4
|
|
|
5
5
|
Tokens never belong in the project. User tokens live in `~/.openxiangda/profiles.json`. Shared workspace env values such as `APP_OSS_*` live in `~/.openxiangda/.env` by default, while project `.env` is only a local override.
|
|
6
6
|
|
|
@@ -95,7 +95,7 @@ Environment-managed workspaces add a logical application and target-specific bin
|
|
|
95
95
|
## Rules
|
|
96
96
|
|
|
97
97
|
- Profile is the deployment boundary.
|
|
98
|
-
- For an environment-managed logical application, the selected target is the deployment boundary inside the profile. `preproduction` and `production` own independent appType, resource IDs,
|
|
98
|
+
- For an environment-managed logical application, the selected target is the deployment boundary inside the profile. `preproduction` and `production` own independent appType, resource IDs, Release Heads, data, and side-effect policy. Their volatile deployment progress is isolated in `.openxiangda/releases/targets/`.
|
|
99
99
|
- `baseUrl` is the backend API base. On standard private deployments it is `<origin>/service`; management pages use `/platform`, and app runtime pages use `/view`.
|
|
100
100
|
- Local state is authoritative for app binding. If a workspace has no `appType` for the target profile, create a new app/workspace with `openxiangda workspace init <dir> --profile <name> --app-name "应用名称"`.
|
|
101
101
|
- Do not search platform apps or reuse similar names unless the user explicitly asks to reuse an existing app or provides an `appType`.
|
|
@@ -108,6 +108,7 @@ Environment-managed workspaces add a logical application and target-specific bin
|
|
|
108
108
|
- This protects the ID map from truncation/lost updates. Concurrent development still uses separate Git worktrees/branches; `.openxiangda/worktree-owner.json` binds one worktree to one active `CODEX_THREAD_ID` and blocks a second task at SDD context/publish time.
|
|
109
109
|
- A prod publish must not read dev IDs.
|
|
110
110
|
- Never copy target resource bindings. Use `openxiangda environment use <target>` or pass `--environment <target>` and let the CLI populate that target from its own deployment.
|
|
111
|
-
- Managed targets cannot use direct `release publish`; use two-phase `release ship`. The first invocation only prepares preproduction and stops; after confirming the preproduction result, a separate invocation with `--confirm-production` promotes the same candidate. Human acceptance is recommended and may be recorded with optional `--acceptance-note`.
|
|
111
|
+
- Managed targets cannot use direct `release publish`; use two-phase `release ship`. The first invocation only prepares preproduction and stops; after confirming the preproduction result, a separate invocation with `--confirm-production` promotes the same candidate. A frozen baseline-adoption intent is automatically reused from `ship.json`; an explicitly repeated but different intent fails before a request. Human acceptance is recommended and may be recorded with optional `--acceptance-note`.
|
|
112
|
+
- In a managed workspace with multiple targets, `function invoke` must include `--environment <target>` and reports the resolved target on stderr before sending the request.
|
|
112
113
|
- Change a single target's side-effect policy with `openxiangda environment policy update <kind|id> --side-effect-policy-json <JSON|file> --reason "..." --dry-run`, inspect the exact diff, then repeat without `--dry-run`. The write uses the server revision as CAS and updates local `revision`; it does not swap roles or alter Release Heads. Omitted fields are preserved unless `--full-replace` is explicit, and production additionally requires `--confirm-production`.
|
|
113
114
|
- Before publishing to another platform, run `openxiangda workspace bind --profile <name> --app-type <APP_XXX>`.
|
|
@@ -109,7 +109,7 @@ openxiangda release ship --change <release-change> --profile <name> \
|
|
|
109
109
|
|
|
110
110
|
Once `environment init` registers a logical application, or `environment attach` connects an existing workspace, `preproduction` and `production` are separate target bindings with separate app/resource/data IDs. Never copy IDs between them. Existing legacy resource mappings may seed only the target whose appType matches (normally preproduction); production stays empty. The normal path is the two-phase `release ship`. Its first invocation creates and deploys the immutable candidate only to preproduction and stops at `awaiting_production_confirmation`. A different, later invocation with `--confirm-production` promotes the exact same candidate and fresh deployment-bound evidence. Human acceptance is recommended and an optional `--acceptance-note` records it, but it is not a universal hard platform gate for explicitly authorized low-risk or emergency releases. Direct `release publish` to either managed target fails closed. Only authorized commissioning/reclassification may run `environment swap --reason "..." --confirm-production`; the atomic operation preserves each app's data and Release Heads, remaps local resources by appType, and keeps existing side-effect restrictions unless explicit replacement policies are supplied. A single environment policy change uses `environment policy update <kind|id>` with a GET-only `--dry-run` first; patch mode validates only caller-supplied fields and preserves unrecognized historical fields, while `--full-replace` validates the supplied complete target and intentionally removes omissions. It uses revision CAS and never swaps roles or Release Heads. Production writes require `--confirm-production`. `organizationWrites=explicit_capability_only` only removes the environment deny and never bypasses `app:organization:manage`. Keep release hard gates limited to explicit scope/profile/target, authorization, clean pushed mainline, immutable identity, CAS/lease, and production confirmation; prose and human acceptance notes are advisory unless strict mode was intentionally configured. `openxiangda studio` is the local loopback-only developer view and exposes only registered safe actions.
|
|
111
111
|
|
|
112
|
-
When a reviewed catch-up contains exact non-delete targets that are already on authoritative mainline but the active application combines several historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates the pair before candidate/deployment creation and forwards it only to exact scoped resource stages. It does not relax frozen online heads, change/lease ownership, delete/prune/force rejection, server CAS, staged-child verification, or the single atomic App finalize.
|
|
112
|
+
When a reviewed catch-up contains exact non-delete targets that are already on authoritative mainline but the active application combines several historical release lineages, the first ship invocation may add `--adopt-online-baseline --adoption-reason "..."`. Ship validates and freezes the pair before candidate/deployment creation. The later `--confirm-production` invocation automatically reuses the same intent from `ship.json` and forwards it only to exact scoped resource stages. It does not relax frozen online heads, change/lease ownership, delete/prune/force rejection, server CAS, staged-child verification, or the single atomic App finalize.
|
|
113
113
|
|
|
114
114
|
An environment-managed release may intentionally replace complete Function/Automation manifests with `release ship --replace-manifest --reason "..."`. The two flags are inseparable, the reason is at least 8 characters, only an exact Backend stage receives them, and production confirmation must repeat the exact preproduction intent. They never widen Form, Workflow, Runtime, configuration, wildcard, or app-wide stages.
|
|
115
115
|
|
|
@@ -137,7 +137,9 @@ openxiangda platform use dev
|
|
|
137
137
|
|
|
138
138
|
Root, `/platform`, and `/view` URLs normalize to `<origin>/service`. Normal work uses the user's profile token and `/openxiangda-api/v1`; never ask for AK/SK. Each profile has independent `appType` and resource IDs.
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Durable local resource/environment mappings live in `.openxiangda/state.json`; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal. User tokens live in `~/.openxiangda/profiles.json`; shared environment values live in `~/.openxiangda/.env`. Never copy resource IDs between profiles or put tokens in the workspace.
|
|
141
|
+
|
|
142
|
+
For an environment-managed workspace with more than one target, `function invoke` requires an explicit `--environment <target>` and prints the resolved profile, environment, kind, appType, and environmentId to stderr before the request. Treat this line as part of the diagnostic evidence; do not infer production behavior from an invocation whose resolved target is preproduction.
|
|
141
143
|
|
|
142
144
|
## Workspace creation/binding
|
|
143
145
|
|
package/package.json
CHANGED
|
@@ -35,7 +35,9 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
35
35
|
- Select logical resource codes with `--only`, or one code with `--code`; type-wide/app-wide release requires an approved dependency closure.
|
|
36
36
|
- Source-triggered Function/Automation publishing uses server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses Backend Release manifest create without `--replace-manifest`; replacing an existing whole manifest requires `--replace-manifest --reason "..."`. On `SOURCE_BASE_DIVERGED` or `RESOURCE_FIELD_CONFLICT`, reconcile, rebuild, and re-plan.
|
|
37
37
|
- A staged FormRelease for the same change may be rebound to a fresh baseline/session only after server verification of immutable/inactive/non-aborted state, identity/hash, frozen schema/formType, finalized resources, parent/base revision, and current Form head. `schemaSyncedAt` is not release evidence; never synthesize it, direct-publish the schema, or activate the Form early to bypass Workflow validation.
|
|
38
|
+
- Managed `release ship --adopt-online-baseline --adoption-reason "..."` freezes that audited intent in the private ship journal; the later `--confirm-production` automatically reuses it and rejects an explicitly different pair before any request.
|
|
38
39
|
- Managed `release ship --replace-manifest --reason "..."` requires the pair, reason length >= 8, and exact Backend selectors; production confirmation repeats the exact preproduction pair, with no forwarding to Form/Workflow/Runtime/config/all stages.
|
|
40
|
+
- In a managed workspace with multiple targets, `function invoke` must include `--environment <target>`; verify the resolved target line on stderr before drawing conclusions from the response.
|
|
39
41
|
- React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
|
|
40
42
|
- Use `AttachmentField` / `ImageField` for form-context previews, and `AttachmentPreviewList` / `ImagePreviewGrid` / `useFilePreview` for standalone custom pages. Do not fake form context, import internal preview implementations, or maintain local previewable-extension lists.
|
|
41
43
|
- Backend permissions, public-access grants, and App Function role/scope checks are authoritative.
|
|
@@ -31,7 +31,9 @@ This is an OpenXiangda React SPA workspace. Read [AGENTS.md](AGENTS.md) for full
|
|
|
31
31
|
- 默认按逻辑资源 code 使用 `--only` 或单资源 `--code`;全类型/全应用发布必须由批准的依赖闭包明确覆盖。
|
|
32
32
|
- Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create,只有替换已有整包 manifest 才必须加 `--replace-manifest --reason "..."`。
|
|
33
33
|
- 相同 change 的 staged FormRelease 只有经服务端重新核验 immutable/inactive/non-aborted、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才可重挂接新 baseline/session。`schemaSyncedAt` 不是发布证据;禁止伪造、直发 schema 或提前激活 Form 绕过 Workflow 校验。
|
|
34
|
+
- 环境托管 `release ship --adopt-online-baseline --adoption-reason "..."` 会把审计意图冻结进私有 ship journal,后续 `--confirm-production` 自动复用;显式传入不同参数会在任何请求前失败。
|
|
34
35
|
- 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
36
|
+
- 多 target 的托管工作区执行 `function invoke` 必须显式传 `--environment <target>`,并先核对 stderr 回显的最终 target 再判断响应。
|
|
35
37
|
- Promotion 必须持有 `release begin/end` 租约;`release begin` 只接受与权威远端 tip 完全一致的 clean main/master。feature branch 或未 push 主线在任何写入前失败。clone primary 只有在冻结仓库 ID 已存在于 `repoAliases` 时才会 canonicalize;无交集在 Release prepare 前失败关闭。激活后直接运行 `integration-status` 和 `release end`,不再补做发布后合并。
|
|
36
38
|
- 已有工作区通过 `environment attach` 接入环境组,旧资源映射只迁移到 appType 相同的预发 target,正式 target 必须为空。preproduction / production 分别拥有独立 appType、资源 ID、数据和副作用策略;禁止直接 `release publish`,禁止跨环境复制 ID,使用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
|
|
37
39
|
- 单独修改某个环境的副作用策略只能使用 `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`:先执行 `--dry-run`,正式环境额外要求 `--confirm-production`,不得借用 `environment swap`。patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段。`organizationWrites=explicit_capability_only` 只解除环境级 deny,仍强制 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。
|
|
@@ -55,7 +55,7 @@ openxiangda commands --json
|
|
|
55
55
|
|
|
56
56
|
工作区一旦通过 `environment init` 登记,或通过 `environment attach` 接入已有环境组,`release publish` 即不再是入口。日常使用两段式 `release ship`:第一次冻结不可变 candidate 并只部署到 preproduction,停止在 `awaiting_production_confirmation`;确认预发结果后,第二次命令提供 `--confirm-production`,把同一 candidate 晋级 production,不会重新构建。真实人工验收是默认建议,可用可选的 `--acceptance-note` 留痕;但用户明确授权的低风险或紧急发布不受僵硬审批门禁阻塞。两套环境的 appType、资源 ID、数据和副作用策略完全独立;旧单目标资源映射只允许迁移到 appType 相同的预发 target,正式 target 必须为空。仅在用户明确授权的投产前重分类中使用 `environment swap --reason "..." --confirm-production` 原子交换两个既有应用的环境角色;数据和 Release Head 不移动,副作用默认不放开。单独调整某个环境的副作用策略时,先运行 `environment policy update <kind|id> ... --dry-run` 查看差异,再按 CAS revision 写入;patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段。不得借用 swap,正式写入另需 `--confirm-production`。`organizationWrites=explicit_capability_only` 仍强制 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。`openxiangda studio` 提供仅本机访问的开发者页面,用于查看绑定、漂移、候选、部署、证据和下一安全动作。
|
|
57
57
|
|
|
58
|
-
只有已审计代码早已进入权威主线、而精确非删除目标在线上来自多次历史发布且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."
|
|
58
|
+
只有已审计代码早已进入权威主线、而精确非删除目标在线上来自多次历史发布且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`。该意图会冻结进私有 `ship.json`,后续 `--confirm-production` 自动复用;它不会放松冻结 online heads、change/lease、删除/全量拒绝、服务端 CAS、staged children 或单次 App finalize 原子激活。
|
|
59
59
|
|
|
60
60
|
环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只会进入精确 Backend selector,正式确认必须复用与预发完全相同的参数,绝不扩散到表单、流程、Runtime、配置资源或全量范围。
|
|
61
61
|
|
|
@@ -39,7 +39,8 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
|
|
|
39
39
|
- Account/role/permission/RBAC/organization-account/query-param authorization work must choose a mode first: `managed-platform-account`, `existing-platform-user-assignment`, `static-role-permission`, or `query-param-context`, then write the permission matrix.
|
|
40
40
|
- Roles that create roles, assign members, grant API permissions, maintain permission groups, or manage organization accounts must declare `apiPermissionCodes` in `src/resources/roles/<code>.json`, such as `app:role:manage`, `app:page-permission-group:manage`, `app:form-permission-group:manage`, and `app:organization:manage`.
|
|
41
41
|
- Confirm current profile before any write: `openxiangda env --profile <name>`.
|
|
42
|
-
- `.openxiangda/state.json` is the authoritative profile → appType → resource-ID map; CLI maintains
|
|
42
|
+
- `.openxiangda/state.json` is the authoritative durable profile → appType → resource-ID map; volatile release/deployment progress stays in the private `.openxiangda/releases/` journal. CLI maintains both; do not hand-edit.
|
|
43
|
+
- In a managed workspace with multiple targets, pass `--environment <target>` to `function invoke` and verify the resolved target printed on stderr.
|
|
43
44
|
- User tokens live in `~/.openxiangda/profiles.json`; shared env in `~/.openxiangda/.env`.
|
|
44
45
|
- Form fields need user-facing `placeholder`; use `SelectField` / `RadioField` for enums, `linkedForm` SelectField for cross-form sources.
|
|
45
46
|
- Form-entry component priority: OpenXiangda platform components → `antd` / `antd-mobile` wrappers → custom business component only when neither fits.
|
|
@@ -39,7 +39,8 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
|
|
|
39
39
|
- 账号/角色/权限/RBAC/组织账号/查询参数授权需求先选权限模式:`managed-platform-account` / `existing-platform-user-assignment` / `static-role-permission` / `query-param-context`,并输出权限矩阵。
|
|
40
40
|
- 角色能新增角色、分配成员、授接口权限、维护权限组或管理组织账号时,`src/resources/roles/<code>.json` 必须声明 `apiPermissionCodes`,例如 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`。
|
|
41
41
|
- 任何写操作前确认当前 profile:`openxiangda env --profile <name>`。
|
|
42
|
-
- `.openxiangda/state.json` 是 profile 到 appType / 资源 ID
|
|
42
|
+
- `.openxiangda/state.json` 是 profile 到 appType / 资源 ID 的持久权威映射;临时 release/deployment 进度只写私有 `.openxiangda/releases/` journal。两者均由 CLI 维护,不要手改。
|
|
43
|
+
- 多 target 的托管工作区执行 `function invoke` 必须显式传 `--environment <target>`,并核对 stderr 回显的最终 target。
|
|
43
44
|
- 用户 token 在 `~/.openxiangda/profiles.json`;共享 env 在 `~/.openxiangda/.env`。
|
|
44
45
|
- 表单字段必须有 user-facing `placeholder`;选项使用 `SelectField` / `RadioField`,跨表用 `linkedForm` SelectField。
|
|
45
46
|
- 表单录入组件顺序:OpenXiangda 平台组件 → `antd` / `antd-mobile` 包装 → 必要时自定义业务组件。
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
- ✅ 相同 change 已有 staged FormRelease 时,不要直发 schema、伪造 `schemaSyncedAt` 或提前激活 Form。CLI 只在重新核验服务端不可变状态、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才将 child 重挂接到新的 baseline/session;冲突继续失败关闭。
|
|
60
60
|
- ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name>`。
|
|
61
61
|
- ✅ 已通过 `environment init` 登记或 `environment attach` 接入的工作区默认使用两段式 `release ship`。第一次命令只冻结 candidate 并部署 preproduction,停止等待正式晋级确认;确认预发结果后,另一次命令提供 `--confirm-production`,即可晋级同一 candidate。人工验收是默认建议,可用可选的 `--acceptance-note` 留痕,但不是所有低风险或紧急发布的硬审批门禁。preproduction / production 的 appType、资源 ID、数据和副作用策略完全隔离,严禁跨环境复制 ID 或直接 `release publish`。旧单目标映射只允许按相同 appType 迁移到预发。仅在用户明确授权的投产前重分类中使用 `environment swap --reason "..." --confirm-production` 原子交换两个既有应用的环境角色;数据和 Release Head 不移动,副作用默认不放开。单环境副作用策略用 `environment policy update <kind|id> ... --dry-run` 预览后按 revision CAS 写入,禁止借用 swap;patch 只校验本次提交字段并原样保留未知历史字段,`--full-replace` 才按完整目标删除遗漏字段,正式写入需 `--confirm-production`,且 `organizationWrites=explicit_capability_only` 不绕过 `app:organization:manage`。发布硬门禁只保留明确 scope/profile/target、权限、干净且已推送主线、不可变版本、CAS/租约与生产确认;文案和人工验收说明默认是建议,只有显式 strict 模式才阻断。
|
|
62
|
-
- ✅ 只有已审计目标早已进入权威主线、线上却由多次历史 lineage 组成且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."
|
|
62
|
+
- ✅ 只有已审计目标早已进入权威主线、线上却由多次历史 lineage 组成且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`;该意图冻结进私有 `ship.json` 并由后续 `--confirm-production` 自动复用。仅允许精确非删除 selectors,冻结 Head、change/lease、服务端 CAS、staged children 与单次 App finalize 仍是硬门禁。
|
|
63
63
|
- ✅ 环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只透传精确 Backend selector,正式确认必须复用同一对参数,不扩散到 Form/Workflow/Runtime/配置或全量范围。
|
|
64
64
|
- ✅ 受支持的配置资源(如 `publicAccessPolicies`)以精确 `resourceSelectors` 为边界;历史 `resources=true` 类别标记会被精确 selector 收窄。缺失 selector、未知类型、通配符 `*`、删除和真正全量资源仍 fail closed。
|
|
65
65
|
- ✅ 本地开发者可运行 `openxiangda studio` 查看两套环境、差异、候选、部署和测试证据;该页面只监听回环地址且只暴露注册动作,生产操作仍需显式确认。
|