openxiangda 1.0.207 → 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 +3 -3
- package/lib/application-environments.js +121 -0
- package/lib/cli.js +243 -62
- package/openxiangda-skills/SKILL.md +3 -3
- package/openxiangda-skills/references/workspace-state.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +6 -2
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +3 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -1
- package/templates/openxiangda-react-spa/AGENTS.md +2 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +3 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +3 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
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
|
|
|
@@ -202,7 +202,7 @@ openxiangda resource publish function --code customer_get --change <change> --pr
|
|
|
202
202
|
|
|
203
203
|
Exact `--only` / `--code` selectors are applied before unrelated manifests, source dependencies, and JS_CODE targets are read or built. Shared/transitive dependencies of the selected targets remain in scope; omitting a selector intentionally preserves full-workspace validation and planning. Resource commands use the packaged canonical scoped builder for standard workspaces, so an older checked-in `scripts/build-js-code.mjs` does not need to be upgraded before the installed CLI gains this optimization; refresh the workspace template only when developers also need the same behavior from a manual `pnpm build-js-code` command.
|
|
204
204
|
|
|
205
|
-
For source-only Function/Automation changes, the final command does not reconstruct whole definitions with client-side GET+PUT. `release begin` captures the Git/change baseline, one preflight covers every selected code, and Backend Release performs one `prepare -> verify -> activate` sequence for all eligible updates. Inspect history with `openxiangda release backend-head|backend-list|backend-detail`, compare it with `backend-diff`, or create an audited immutable rollback with `backend-rollback <releaseId> --change <change> --reason "..."`. Always merge/push the frozen SHA and run `release end` when promotion finishes. A lost or expired lease retains the pending-mainline evidence instead of silently clearing it.
|
|
205
|
+
For source-only Function/Automation changes, the final command does not reconstruct whole definitions with client-side GET+PUT. `release begin` captures the Git/change baseline, one preflight covers every selected code, and Backend Release performs one `prepare -> verify -> activate` sequence for all eligible updates. Repository identity is alias-aware but fail-closed: when a clone-derived primary ID differs from the frozen source base, the CLI uses the frozen canonical ID only if it is present in `releaseSourceRevision.repoAliases`; Backend, Workflow, and Root App Release writes all carry that same ID, while a disjoint identity set fails before prepare. Inspect history with `openxiangda release backend-head|backend-list|backend-detail`, compare it with `backend-diff`, or create an audited immutable rollback with `backend-rollback <releaseId> --change <change> --reason "..."`. Always merge/push the frozen SHA and run `release end` when promotion finishes. A lost or expired lease retains the pending-mainline evidence instead of silently clearing it.
|
|
206
206
|
|
|
207
207
|
Page repair publishing is staged by default. It first freezes `pages/snapshot`, sends the active Page Release parent plus every page revision, and uses revision `0` only for a genuinely new page. Review with `openxiangda page head|releases|detail|diff`; activate an immutable complete release explicitly with `page activate <releaseId> --change <change>`. Historical activation requires `page rollback <releaseId> --rollback --change <change> --reason "..."`. Parent or revision conflicts are never refreshed or retried automatically.
|
|
208
208
|
|
|
@@ -247,6 +247,53 @@ function normalizeManagedReleaseSourceRevision(
|
|
|
247
247
|
};
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
function normalizeReleaseSourceRevisionForBaseline(
|
|
251
|
+
target,
|
|
252
|
+
sourceBase,
|
|
253
|
+
sourceRevision,
|
|
254
|
+
options = {}
|
|
255
|
+
) {
|
|
256
|
+
const revision = normalizeManagedReleaseSourceRevision(
|
|
257
|
+
target,
|
|
258
|
+
sourceRevision,
|
|
259
|
+
options
|
|
260
|
+
);
|
|
261
|
+
const expectedRepositoryId = String(
|
|
262
|
+
sourceBase?.repo || sourceBase?.repositoryId || ''
|
|
263
|
+
)
|
|
264
|
+
.trim()
|
|
265
|
+
.toLowerCase();
|
|
266
|
+
if (!expectedRepositoryId) return revision;
|
|
267
|
+
const repositoryAliases = normalizeIdentityList([
|
|
268
|
+
revision?.repo,
|
|
269
|
+
revision?.repositoryId,
|
|
270
|
+
...(Array.isArray(revision?.repoAliases) ? revision.repoAliases : []),
|
|
271
|
+
]);
|
|
272
|
+
if (!repositoryAliases.includes(expectedRepositoryId)) {
|
|
273
|
+
const code =
|
|
274
|
+
String(options.errorCode || '').trim() ||
|
|
275
|
+
'RELEASE_SOURCE_REPOSITORY_MISMATCH';
|
|
276
|
+
const error = new Error(
|
|
277
|
+
`${code}: 当前 Git 仓库与冻结 change baseline 仓库不一致`
|
|
278
|
+
);
|
|
279
|
+
error.code = code;
|
|
280
|
+
error.details = {
|
|
281
|
+
expectedRepositoryId,
|
|
282
|
+
repositoryAliases,
|
|
283
|
+
};
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...revision,
|
|
288
|
+
repo: expectedRepositoryId,
|
|
289
|
+
repositoryId: expectedRepositoryId,
|
|
290
|
+
repoAliases: normalizeIdentityList([
|
|
291
|
+
expectedRepositoryId,
|
|
292
|
+
...repositoryAliases,
|
|
293
|
+
]),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
250
297
|
function normalizeManagedChangeSourceBase(
|
|
251
298
|
target,
|
|
252
299
|
sourceBase,
|
|
@@ -427,6 +474,76 @@ function readCandidate(candidateId, cwd = process.cwd()) {
|
|
|
427
474
|
}
|
|
428
475
|
}
|
|
429
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
|
+
|
|
430
547
|
function readJsonInput(value, label, cwd = process.cwd()) {
|
|
431
548
|
const input = String(value || '').trim();
|
|
432
549
|
if (!input) throw new Error(`${label} 不能为空`);
|
|
@@ -597,12 +714,16 @@ module.exports = {
|
|
|
597
714
|
normalizeEnvironmentKind,
|
|
598
715
|
normalizeManagedChangeSourceBase,
|
|
599
716
|
normalizeManagedReleaseSourceRevision,
|
|
717
|
+
normalizeReleaseSourceRevisionForBaseline,
|
|
600
718
|
normalizeSideEffectPolicyInput,
|
|
601
719
|
normalizeTargetName,
|
|
602
720
|
hasStateResourceMappings,
|
|
721
|
+
managedTargetDeploymentFile,
|
|
603
722
|
readCandidate,
|
|
604
723
|
readJsonInput,
|
|
724
|
+
readRememberedTargetDeployment,
|
|
605
725
|
releaseExecutionPath,
|
|
726
|
+
rememberTargetDeployment,
|
|
606
727
|
resolveManagedStateBinding,
|
|
607
728
|
resolveEnvironmentTarget,
|
|
608
729
|
saveCandidate,
|
package/lib/cli.js
CHANGED
|
@@ -136,9 +136,12 @@ const {
|
|
|
136
136
|
normalizeEnvironmentKind,
|
|
137
137
|
normalizeManagedChangeSourceBase,
|
|
138
138
|
normalizeManagedReleaseSourceRevision,
|
|
139
|
+
normalizeReleaseSourceRevisionForBaseline,
|
|
139
140
|
readCandidate,
|
|
140
141
|
readJsonInput,
|
|
142
|
+
readRememberedTargetDeployment,
|
|
141
143
|
releaseExecutionPath,
|
|
144
|
+
rememberTargetDeployment,
|
|
142
145
|
resolveManagedStateBinding,
|
|
143
146
|
resolveEnvironmentTarget,
|
|
144
147
|
saveCandidate,
|
|
@@ -1103,6 +1106,128 @@ function writeManagedShip(changeId, value) {
|
|
|
1103
1106
|
};
|
|
1104
1107
|
}
|
|
1105
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
|
+
|
|
1106
1231
|
function writePrivateJsonAtomic(file, value) {
|
|
1107
1232
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1108
1233
|
const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -2716,14 +2841,6 @@ function applicationDeploymentApiPath(logicalAppCode, deploymentId, action) {
|
|
|
2716
2841
|
return environmentSetApiPath(logicalAppCode, suffix);
|
|
2717
2842
|
}
|
|
2718
2843
|
|
|
2719
|
-
function rememberTargetDeployment(target, deployment) {
|
|
2720
|
-
target.bound.lastDeploymentId = deployment.id;
|
|
2721
|
-
target.bound.lastCandidateId = deployment.candidateId || null;
|
|
2722
|
-
target.bound.lastDeploymentStatus = deployment.status;
|
|
2723
|
-
target.bound.updatedAt = new Date().toISOString();
|
|
2724
|
-
saveProjectState(target.state);
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
2844
|
function assertLocalCandidate(candidate) {
|
|
2728
2845
|
if (
|
|
2729
2846
|
!candidate?.id ||
|
|
@@ -2905,7 +3022,7 @@ async function markApplicationDeploymentFailed(
|
|
|
2905
3022
|
error
|
|
2906
3023
|
) {
|
|
2907
3024
|
try {
|
|
2908
|
-
await requestWithAuth(
|
|
3025
|
+
return await requestWithAuth(
|
|
2909
3026
|
config,
|
|
2910
3027
|
target.profileName,
|
|
2911
3028
|
applicationDeploymentApiPath(
|
|
@@ -2929,6 +3046,7 @@ async function markApplicationDeploymentFailed(
|
|
|
2929
3046
|
warn(
|
|
2930
3047
|
`部署失败记录未写入平台: ${maskText(recordError?.message || recordError)}`
|
|
2931
3048
|
);
|
|
3049
|
+
return null;
|
|
2932
3050
|
}
|
|
2933
3051
|
}
|
|
2934
3052
|
|
|
@@ -3002,13 +3120,24 @@ async function deployApplicationCandidate(
|
|
|
3002
3120
|
},
|
|
3003
3121
|
};
|
|
3004
3122
|
}
|
|
3005
|
-
await markApplicationDeploymentFailed(
|
|
3123
|
+
const failedDeployment = await markApplicationDeploymentFailed(
|
|
3006
3124
|
config,
|
|
3007
3125
|
target,
|
|
3008
3126
|
logicalApp.code,
|
|
3009
3127
|
deployment.id,
|
|
3010
3128
|
error
|
|
3011
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';
|
|
3012
3141
|
throw error;
|
|
3013
3142
|
}
|
|
3014
3143
|
}
|
|
@@ -3172,26 +3301,12 @@ async function runApplicationShipCommand(
|
|
|
3172
3301
|
);
|
|
3173
3302
|
const existingShip = readManagedShip(changeId);
|
|
3174
3303
|
const confirmProduction = Boolean(flags['confirm-production']);
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
const recordedReplacement = existingShip.manifestReplacement || {
|
|
3182
|
-
enabled: false,
|
|
3183
|
-
reason: null,
|
|
3184
|
-
};
|
|
3185
|
-
if (
|
|
3186
|
-
Boolean(recordedReplacement.enabled) !== manifestReplacement.enabled ||
|
|
3187
|
-
String(recordedReplacement.reason || '') !==
|
|
3188
|
-
String(manifestReplacement.reason || '')
|
|
3189
|
-
) {
|
|
3190
|
-
fail(
|
|
3191
|
-
'RELEASE_SHIP_REPLACE_MANIFEST_INTENT_MISMATCH: --replace-manifest/--reason 必须与该 candidate 的预发发布意图完全一致;请使用 ship.json 记录的同一对参数'
|
|
3192
|
-
);
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3304
|
+
const intents = resolveManagedShipIntents(flags, existingShip);
|
|
3305
|
+
const {
|
|
3306
|
+
manifestReplacement,
|
|
3307
|
+
baselineAdoption,
|
|
3308
|
+
effectiveFlags,
|
|
3309
|
+
} = intents;
|
|
3195
3310
|
|
|
3196
3311
|
if (confirmProduction && !existingShip?.preproductionDeploymentId) {
|
|
3197
3312
|
fail(
|
|
@@ -3208,7 +3323,7 @@ async function runApplicationShipCommand(
|
|
|
3208
3323
|
config,
|
|
3209
3324
|
preproduction,
|
|
3210
3325
|
{
|
|
3211
|
-
...
|
|
3326
|
+
...effectiveFlags,
|
|
3212
3327
|
change: changeId,
|
|
3213
3328
|
'test-plan-json': JSON.stringify(
|
|
3214
3329
|
managedShipTestPlan()
|
|
@@ -3224,6 +3339,7 @@ async function runApplicationShipCommand(
|
|
|
3224
3339
|
sourceRevision: candidate.sourceRevision,
|
|
3225
3340
|
candidateFile: created.candidateFile,
|
|
3226
3341
|
manifestReplacement,
|
|
3342
|
+
baselineAdoption,
|
|
3227
3343
|
});
|
|
3228
3344
|
} else {
|
|
3229
3345
|
assertLocalCandidate(candidate);
|
|
@@ -3242,13 +3358,34 @@ async function runApplicationShipCommand(
|
|
|
3242
3358
|
!deployment ||
|
|
3243
3359
|
!['deployed', 'succeeded'].includes(deployment.status)
|
|
3244
3360
|
) {
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
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
|
+
}
|
|
3252
3389
|
}
|
|
3253
3390
|
const saved = writeManagedShip(changeId, {
|
|
3254
3391
|
status: 'awaiting_production_confirmation',
|
|
@@ -3261,17 +3398,17 @@ async function runApplicationShipCommand(
|
|
|
3261
3398
|
deployment.targetAppReleaseId || null,
|
|
3262
3399
|
preproductionStatus: deployment.status,
|
|
3263
3400
|
manifestReplacement,
|
|
3401
|
+
baselineAdoption,
|
|
3402
|
+
failure: null,
|
|
3264
3403
|
preparedAt:
|
|
3265
3404
|
existingShip?.preparedAt ||
|
|
3266
3405
|
new Date().toISOString(),
|
|
3267
|
-
nextCommand:
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
)}`
|
|
3274
|
-
: ''),
|
|
3406
|
+
nextCommand: buildManagedShipCommand(
|
|
3407
|
+
changeId,
|
|
3408
|
+
preproduction.profileName,
|
|
3409
|
+
intents,
|
|
3410
|
+
{ confirmProduction: true }
|
|
3411
|
+
),
|
|
3275
3412
|
});
|
|
3276
3413
|
return {
|
|
3277
3414
|
phase: 'preproduction',
|
|
@@ -3393,17 +3530,38 @@ async function runApplicationShipCommand(
|
|
|
3393
3530
|
manualAcceptance,
|
|
3394
3531
|
preproductionStatus: preproductionDeployment.status,
|
|
3395
3532
|
});
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
+
}
|
|
3407
3565
|
const saved = writeManagedShip(changeId, {
|
|
3408
3566
|
status: 'completed',
|
|
3409
3567
|
productionEnvironment: production.targetName,
|
|
@@ -3411,6 +3569,7 @@ async function runApplicationShipCommand(
|
|
|
3411
3569
|
productionAppReleaseId:
|
|
3412
3570
|
promoted.deployment.targetAppReleaseId || null,
|
|
3413
3571
|
productionStatus: promoted.deployment.status,
|
|
3572
|
+
failure: null,
|
|
3414
3573
|
completedAt: new Date().toISOString(),
|
|
3415
3574
|
});
|
|
3416
3575
|
return {
|
|
@@ -3457,7 +3616,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3457
3616
|
readStringFlag(flags, 'deployment') ||
|
|
3458
3617
|
readStringFlag(flags, 'deployment-id') ||
|
|
3459
3618
|
positional[0] ||
|
|
3460
|
-
target
|
|
3619
|
+
readRememberedTargetDeployment(target)?.deploymentId;
|
|
3461
3620
|
const reason = readStringFlag(flags, 'reason');
|
|
3462
3621
|
if (!deploymentId || reason.length < 8) {
|
|
3463
3622
|
fail(
|
|
@@ -3485,7 +3644,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3485
3644
|
readStringFlag(flags, 'candidate') ||
|
|
3486
3645
|
readStringFlag(flags, 'candidate-id') ||
|
|
3487
3646
|
positional[0] ||
|
|
3488
|
-
target
|
|
3647
|
+
readRememberedTargetDeployment(target)?.candidateId;
|
|
3489
3648
|
if (subcommand === 'deploy') {
|
|
3490
3649
|
if (target.environmentKind !== 'preproduction') {
|
|
3491
3650
|
fail('release deploy 只允许目标为 preproduction');
|
|
@@ -3526,7 +3685,7 @@ async function runApplicationEnvironmentReleaseCommand(
|
|
|
3526
3685
|
const deploymentId =
|
|
3527
3686
|
readStringFlag(flags, 'deployment') ||
|
|
3528
3687
|
readStringFlag(flags, 'deployment-id') ||
|
|
3529
|
-
target
|
|
3688
|
+
readRememberedTargetDeployment(target)?.deploymentId;
|
|
3530
3689
|
if (!deploymentId || !flags['evidence-json']) {
|
|
3531
3690
|
fail(
|
|
3532
3691
|
'release test 必须提供 --deployment <id>(首次可省略)和 --evidence-json <JSON|file>'
|
|
@@ -3635,7 +3794,7 @@ async function release(args) {
|
|
|
3635
3794
|
' openxiangda release app-finalize --change <id> --staged-resources-json <JSON|file> --profile <name>',
|
|
3636
3795
|
'说明:',
|
|
3637
3796
|
' - ship 是环境托管应用的两段式快车道:首次只部署预发并停止;确认预发结果后,第二次以同一 candidate 晋级正式。人工验收备注可选。',
|
|
3638
|
-
' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason
|
|
3797
|
+
' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason;预发冻结后,正式确认会从 ship.json 自动复用同一意图,仍保留冻结 Head、CAS、租约、staged children 和单次 App finalize。',
|
|
3639
3798
|
' - ship 的 --replace-manifest 与 --reason 必须成对,只透传给精确 Backend Function/Automation stage;正式确认必须复用与预发完全相同的参数。',
|
|
3640
3799
|
' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
|
|
3641
3800
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
@@ -10718,6 +10877,27 @@ async function appFunction(args) {
|
|
|
10718
10877
|
if (subcommand === 'invoke') {
|
|
10719
10878
|
const [functionCode] = positional;
|
|
10720
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
|
+
);
|
|
10721
10901
|
const body = readDirectJsonBody(flags, 'function invoke', { optional: true });
|
|
10722
10902
|
const openXiangdaApiRequest = buildOpenXiangdaFunctionInvokeRequest(
|
|
10723
10903
|
target.appType,
|
|
@@ -18210,8 +18390,9 @@ async function ensureOwnedImmutableReleasePublishContext(
|
|
|
18210
18390
|
`${releaseKind} 只接受与本地冻结基线匹配的 stored lease;外部 opaque lease 不能用于原子发布`
|
|
18211
18391
|
);
|
|
18212
18392
|
}
|
|
18213
|
-
const sourceRevision =
|
|
18393
|
+
const sourceRevision = normalizeReleaseSourceRevisionForBaseline(
|
|
18214
18394
|
target,
|
|
18395
|
+
baseline.sourceBase || {},
|
|
18215
18396
|
baseline.releaseSourceRevision || {},
|
|
18216
18397
|
{ errorCode: 'RELEASE_SOURCE_REPOSITORY_MISMATCH' }
|
|
18217
18398
|
);
|
|
@@ -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
|
|
|
@@ -118,7 +118,7 @@ Reviewed bundle commands may retain `<profile>` as a template. The explicit real
|
|
|
118
118
|
|
|
119
119
|
Exact `resourceSelectors` are authoritative for supported configuration resources such as `publicAccessPolicies`. A legacy `resources=true` category marker is narrowed by those exact selectors and must not become an app-wide generic-resource release. Missing selectors, unknown resource types, wildcard `*`, deletes, and genuine app-wide resource closures remain fail closed.
|
|
120
120
|
|
|
121
|
-
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences
|
|
121
|
+
`release begin --change` freezes a clean committed `HEAD` only when the current branch is the authoritative default `main`/`master` and its commit exactly equals the live remote tip. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A feature worktree or unpushed main receives `RELEASE_SOURCE_MAINLINE_REQUIRED` / `RELEASE_SOURCE_MAINLINE_NOT_PUSHED`; merge, test, push, and start the one mainline release instead of forcing it. Optional `.git` remote suffix differences and other recorded repository aliases are accepted only when the frozen source-base ID intersects `releaseSourceRevision.repoAliases`; Backend, Workflow, and Root App Release writes then use the frozen canonical ID. Genuinely different identity sets still fail closed before prepare.
|
|
122
122
|
|
|
123
123
|
Because promotion begins from the already-pushed authoritative mainline, `release integration-status` should pass immediately after activation. Run it, then `release end`; no post-release branch merge is required.
|
|
124
124
|
|
|
@@ -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>`.
|
|
@@ -63,6 +63,8 @@ Function/Automation source analysis also extracts statically declared Form filte
|
|
|
63
63
|
|
|
64
64
|
Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix source-backed create, source-free declarative Automation manifest create, source-only update, and manifest replacement update through explicit per-resource `operation/mode`; the CLI freezes the current Backend Release parent plus Git/change baseline, then runs `prepare -> verify -> activate` or stops verified for `--stage-only`. A new Automation with a complete `definitionJson.version="v3"` and no `sourceFile` automatically uses manifest create without `--replace-manifest`; an incomplete definition still fails closed. Activation CAS-checks the entire set and applies all updates in one transaction. `--stage-only` and Secret-bound Function publishing fail closed when Backend Release v2 is unavailable; compatibility fallback is limited to non-staged, non-Secret publishing after an explicit Backend head 404. Existing online bindings, contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged; noops do not advance versions/timestamps. A deliberate whole-definition replacement of an existing resource requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
|
|
65
65
|
|
|
66
|
+
Repository identity remains strict across immutable releases. If a clone-derived primary repository ID differs from the frozen change source base, the CLI may canonicalize Backend, Workflow, and Root App Release payloads to the frozen ID only when that ID is already present in `releaseSourceRevision.repoAliases`; no alias intersection fails with `RELEASE_SOURCE_REPOSITORY_MISMATCH` before prepare.
|
|
67
|
+
|
|
66
68
|
Functions with a top-level `secretRefs` field use `backend_release_v2`, including an explicit empty list that removes bindings. This path never falls back to source PATCH. It requires the per-app Secret capability probe to grant `app_function_secrets_v1`, `trusted_node_v2`, `backend_release_v2`, and `atomic_staged_children_v2`; otherwise plan/publish fails closed.
|
|
67
69
|
|
|
68
70
|
For whole-app atomic activation, publish the exact mixed Function/Automation scope with `resource publish function,automation --only function:<code>,automation:<code> --stage-only --change <change>`. This stops after Backend Release verify and returns `stagedResource` plus every handled selector; the CLI automatically merges it with other changed staged children in `.openxiangda/releases/<change>/staged-resources.json` for `release app-finalize --staged-resources-json`. A Backend Release that was already activated returns `activeResource` and must not be presented as staged.
|
|
@@ -107,7 +109,7 @@ openxiangda release ship --change <release-change> --profile <name> \
|
|
|
107
109
|
|
|
108
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.
|
|
109
111
|
|
|
110
|
-
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.
|
|
111
113
|
|
|
112
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.
|
|
113
115
|
|
|
@@ -135,7 +137,9 @@ openxiangda platform use dev
|
|
|
135
137
|
|
|
136
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.
|
|
137
139
|
|
|
138
|
-
|
|
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.
|
|
139
143
|
|
|
140
144
|
## Workspace creation/binding
|
|
141
145
|
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
26
26
|
- L0 read-only/docs/tests need no SDD; L1 narrow reversible fixes record exact scope without a redundant second confirmation; schema, business Functions, Automation/Workflow, permissions, auth/public access, data writes, and runtime/config are L2/L3 full-SDD work. Live evidence/archive are post-release stages.
|
|
27
27
|
- Very small copy/style/binding changes may use `openxiangda sdd quick <change> ...`; quick mode records exact low-risk scope without a redundant proposal/approval loop when the user already requested it.
|
|
28
28
|
- SDD is streamlined by default: approval and exact structured scope are hard gates, while unfinished task/evidence/spec prose only warns. Configure `strictDocumentation: true` only when prose must block.
|
|
29
|
-
- Before platform writes, run `release begin` from a clean local main/master that exactly equals the authoritative remote tip. Feature branches and unpushed mainline commits fail before any write. After activation, run `integration-status` and `release end`; no post-release merge is needed.
|
|
29
|
+
- Before platform writes, run `release begin` from a clean local main/master that exactly equals the authoritative remote tip. Feature branches and unpushed mainline commits fail before any write. A clone primary may be canonicalized to the frozen repository ID only when that ID is already in `repoAliases`; otherwise release prepare fails closed. After activation, run `integration-status` and `release end`; no post-release merge is needed.
|
|
30
30
|
- Managed preproduction and production targets own independent app/resource/data identities and side-effect policy. Existing workspaces use `environment attach`; only an appType-matching legacy binding may seed preproduction and production starts empty. Never direct-publish or copy IDs across them; use `openxiangda studio` to inspect candidate, evidence, and drift state. Only an explicitly authorized commissioning reclassification may use `environment swap --reason "..." --confirm-production`; it preserves app data/Release Heads and keeps side effects restricted by default.
|
|
31
31
|
- Change one target's side-effect policy only with `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`: run `--dry-run` first, add `--confirm-production` for production, and never use `environment swap` for this. Patch mode validates only supplied fields and preserves unrecognized historical fields; `--full-replace` validates the supplied complete target and intentionally removes omissions. `organizationWrites=explicit_capability_only` removes the environment deny but still requires `app:organization:manage`. Keep release hard gates 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 is configured.
|
|
32
32
|
- Account/role/permission/RBAC/organization-account/query-param authorization work must run `openxiangda design gates --topic permissions --json`, choose `managed-platform-account`, `existing-platform-user-assignment`, `static-role-permission`, or `query-param-context`, and write the permission matrix.
|
|
@@ -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,8 +31,10 @@ 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/配置或全量步骤。
|
|
35
|
-
-
|
|
36
|
+
- 多 target 的托管工作区执行 `function invoke` 必须显式传 `--environment <target>`,并先核对 stderr 回显的最终 target 再判断响应。
|
|
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 模式才阻断。
|
|
38
40
|
- React SPA 路由由 `src/app/router.tsx` 管理,前端包通过 `openxiangda runtime deploy` 发布。
|
|
@@ -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
|
|
|
@@ -82,7 +82,7 @@ App Function 第三方凭据只能在 Function manifest 顶层声明 `secretRefs
|
|
|
82
82
|
|
|
83
83
|
`openxiangda runtime deploy --no-activate` 会构建并上传不可变预览版本;发布前先提交所有可能进入构建的源码/配置。所有 Runtime deploy(包括 `--no-activate`)都会先获取应用发布 lease,并在任何构建和上传前冻结 clean `HEAD` 与当前 active Runtime 父血缘;旧分支返回 `RUNTIME_SOURCE_BASE_DIVERGED`,不能先上传旧 preview 再激活。`openspec/` SDD 证据和生成/状态目录不算源码 dirty。仅审批的回退可使用 `--allow-runtime-rollback --reason "至少 8 个字符"`;`--no-build` 不会跳过守卫。不要手工修改 `dist/index.html`。
|
|
84
84
|
|
|
85
|
-
Function/Automation 走 Backend Release v2;同一个 child 可以混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。声明式 create 自动选路且不需要 `--replace-manifest`;替换已有资源才需要该显式授权。正式多资源发布必须使用 canonical 精确 selector 和 `--stage-only`。`release begin` 只接受与权威远端默认主分支完全一致的 clean HEAD;feature branch 或未 push 的 main
|
|
85
|
+
Function/Automation 走 Backend Release v2;同一个 child 可以混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。声明式 create 自动选路且不需要 `--replace-manifest`;替换已有资源才需要该显式授权。正式多资源发布必须使用 canonical 精确 selector 和 `--stage-only`。`release begin` 只接受与权威远端默认主分支完全一致的 clean HEAD;feature branch 或未 push 的 main 会在任何平台写入前失败。clone primary 与冻结仓库 ID 不同时,只有该冻结 ID 已存在于 `repoAliases` 才会统一用于 Backend/Workflow/Root App Release;无交集继续失败关闭。成功激活后主线证据天然成立,不再补做发布后合并。
|
|
86
86
|
|
|
87
87
|
## 应用结构
|
|
88
88
|
|
|
@@ -32,14 +32,15 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
|
|
|
32
32
|
- Source-triggered Function/Automation publishing uses server-side source-field PATCH by default 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 manifest create; replacing an existing whole manifest requires exact `--only/--code` plus `--replace-manifest --reason "..."`.
|
|
33
33
|
- 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.
|
|
34
34
|
- 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.
|
|
35
|
-
- Before platform writes, run `release begin` only from clean main/master exactly equal to the authoritative remote tip. Feature branches and unpushed mainline commits fail before writes. After activation, run `integration-status` and `release end`; no post-release merge is needed.
|
|
35
|
+
- Before platform writes, run `release begin` only from clean main/master exactly equal to the authoritative remote tip. Feature branches and unpushed mainline commits fail before writes. A clone primary may be canonicalized to the frozen repository ID only when that ID is already in `repoAliases`; otherwise release prepare fails closed. After activation, run `integration-status` and `release end`; no post-release merge is needed.
|
|
36
36
|
- Environment-managed workspaces never publish directly. Existing workspaces use `environment attach`; only an appType-matching legacy binding may seed preproduction and production starts empty. Run `release candidate`, `release deploy --environment preproduction`, `release test`, then promote that same candidate with `release promote --environment production --confirm-production`. Keep app/resource/data IDs isolated and inspect state with `openxiangda studio`. Only an explicitly authorized commissioning reclassification may use `environment swap --reason "..." --confirm-production`; it preserves app data/Release Heads and keeps side effects restricted by default.
|
|
37
37
|
- Change one target's side-effect policy only with `environment policy update <preproduction|production> --side-effect-policy-json <JSON|file> --reason "..."`: run `--dry-run` first, add `--confirm-production` for production, and never use `environment swap` for this. Patch mode validates only supplied fields and preserves unrecognized historical fields; `--full-replace` validates the supplied complete target and intentionally removes omissions. `organizationWrites=explicit_capability_only` removes the environment deny but still requires `app:organization:manage`. Keep release hard gates 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 is configured.
|
|
38
38
|
- Routine edits should plan and publish exact change targets: `workspace plan --profile <name> --change <change> --changed`, then `workspace publish --profile <name> --change <change> --only pages/a,forms/b --dry-run`.
|
|
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.
|
|
@@ -32,14 +32,15 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
|
|
|
32
32
|
- Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state;无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create,只有替换已有整包 manifest 才必须精确 `--only/--code` 并加 `--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
34
|
- 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
35
|
-
- 写入平台前只从与权威远端 tip 完全一致的 clean main/master 执行 `release begin`。feature branch 或未 push
|
|
35
|
+
- 写入平台前只从与权威远端 tip 完全一致的 clean main/master 执行 `release begin`。feature branch 或未 push 主线在任何写入前失败;clone primary 只有在冻结仓库 ID 已存在于 `repoAliases` 时才会 canonicalize,无交集在 Release prepare 前失败关闭;激活后直接运行 `integration-status` 和 `release end`,无需发布后再合并。
|
|
36
36
|
- 环境托管工作区禁止直发:已有工作区先用 `environment attach` 接入,旧映射只能迁移到 appType 相同的预发 target,正式 target 必须为空。然后执行 `release candidate`、`release deploy --environment preproduction`、`release test`,最后将同一 candidate 用 `release promote --environment production --confirm-production` 晋级。两套环境的 appType、资源 ID 和数据不可互拷;用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
|
|
37
37
|
- 单独修改某个环境的副作用策略只能使用 `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 模式才阻断。
|
|
38
38
|
- 单文件改动默认按 change 和逻辑目标发布:先 `workspace plan --profile <name> --change <change> --changed`,再 `workspace publish --profile <name> --change <change> --only pages/a,forms/b --dry-run`。
|
|
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` 包装 → 必要时自定义业务组件。
|
|
@@ -55,11 +55,11 @@
|
|
|
55
55
|
- ✅ 发现平台缺陷、能力缺口、规则不清、反复 workaround、AI 不确定点、用户可见体验问题时,主动 `openxiangda feedback submit --yes`;发送后告诉用户反馈内容和 fingerprint。
|
|
56
56
|
- ✅ 正式多资源开发优先写 `src/resources/**` 后执行 `openxiangda resource validate|plan|publish <type> --only <codes>`;单资源可用 `--code <code>`。直接 CLI 写平台资源时先 `--dry-run`,需要避免漂移就加 `--write-manifest`。
|
|
57
57
|
- ✅ `resource plan` 与 publish dry-run 严格只允许 GET/HEAD;遇到 `READ_ONLY_AUTH_REQUIRED` 时先执行 `openxiangda auth refresh --profile <name>` 或重新登录,不得在 plan 内自动 POST 刷新 token。
|
|
58
|
-
- ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。声明式 create 自动选路;替换已有资源的整包 manifest 才需要另加 `--replace-manifest --reason "..."`。
|
|
58
|
+
- ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合源码 create、无 `sourceFile` 的完整 v3 声明式 Automation manifest create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。声明式 create 自动选路;替换已有资源的整包 manifest 才需要另加 `--replace-manifest --reason "..."`。clone primary 与冻结仓库 ID 不同时,只有冻结 ID 已存在于 `repoAliases` 才会统一用于 Backend/Workflow/Root App Release;无交集继续失败关闭。
|
|
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` 查看两套环境、差异、候选、部署和测试证据;该页面只监听回环地址且只暴露注册动作,生产操作仍需显式确认。
|