openxiangda 1.0.204 → 1.0.207
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/lib/application-environments.js +16 -10
- package/lib/cli.js +560 -58
- package/openxiangda-skills/SKILL.md +5 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +3 -1
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +2 -0
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +3 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +1 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -1
- package/templates/openxiangda-react-spa/AGENTS.md +4 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +1 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +3 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +1 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +3 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
|
|
|
82
82
|
|
|
83
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.
|
|
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. 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 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.
|
|
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
|
|
|
@@ -135,7 +135,7 @@ openxiangda policy render
|
|
|
135
135
|
entrypoints. Generated documents carry `OpenXiangda-Policy-Version`; edit the
|
|
136
136
|
canonical policy first instead of independently changing duplicate instructions.
|
|
137
137
|
|
|
138
|
-
Source-triggered Function and Automation publishing is source-only by default. Backend Release v2 can mix create, source-only update, and manifest replacement update in one immutable child; a stale target therefore causes zero resource writes instead of failing halfway through 88 Functions and 11 Automations. `--stage-only` stops after verification, reports all handled selectors, and never falls back to direct Function/Automation writes when the platform is missing or incompatible. The release preserves online `resourceBindings`, input/output contracts, descriptive metadata, trigger/view configuration, and enabled/published state, and planned noops do not advance resource versions or timestamps. To intentionally make a complete local manifest replace
|
|
138
|
+
Source-triggered Function and Automation publishing is source-only by default. Backend Release v2 can mix source-backed create, source-free declarative Automation manifest create, source-only update, and manifest replacement update in one immutable child; a stale target therefore causes zero resource writes instead of failing halfway through 88 Functions and 11 Automations. A new Automation with a complete `definitionJson.version="v3"` definition and no `sourceFile` automatically uses manifest create and does not require `--replace-manifest`; incomplete definitions still fail closed. `--stage-only` stops after verification, reports all handled selectors, and never falls back to direct Function/Automation writes when the platform is missing or incompatible. The release preserves online `resourceBindings`, input/output contracts, descriptive metadata, trigger/view configuration, and enabled/published state, and planned noops do not advance resource versions or timestamps. To intentionally make a complete local manifest replace an existing online definition, select exact codes and opt in explicitly with `--replace-manifest --reason "<why this replacement is safe>"`; `--sdd-bypass` does not widen that scope. Standard workspaces use the CLI-bundled scoped builder without spawning workspace `pnpm`, and `source_lineage_v1` compares authored source/dependencies independently from generated bundle bytes.
|
|
139
139
|
|
|
140
140
|
React SPA templates include stable Vite manual chunk grouping for React, antd, ECharts, editor dependencies, and OpenXiangda runtime/component entrypoints. The SDK keeps large UI dependencies such as `antd-mobile` and `dayjs` external so the application bundler can split them by route. Applications should import from public package entrypoints such as `openxiangda`, `openxiangda/runtime`, and `openxiangda/runtime/react`; do not import `openxiangda/packages/sdk/dist/...` internals to chase bundle size.
|
|
141
141
|
|
|
@@ -208,6 +208,8 @@ Page repair publishing is staged by default. It first freezes `pages/snapshot`,
|
|
|
208
208
|
|
|
209
209
|
Use `openxiangda release app-capture` to read the platform's transactionally consistent whole-app manifest. For a normal multi-resource release, stage the changed Runtime/Page/Backend/Form child releases; commands carrying the same `--change` atomically accumulate their canonical immutable entries in `.openxiangda/releases/<change>/staged-resources.json`. Then run `app-finalize --change <change> --staged-resources-json <JSON|file>`. The CLI overlays Runtime/Page/Backend by singleton kind and Form by `formUuid`, preserves every unmodified active child from the read-only capture, and sends the complete frozen manifest through `prepare -> verify -> activate` with `activateStagedChildren=true`. Runtime staging never rewrites local active Runtime state, and a malformed staged response fails closed. The child heads and App head therefore switch in one database transaction (`atomic_staged_children_v1`); any parent, revision, asset, or hash drift stops with zero root activation and is never refreshed or retried. An aborted Form Release is never an idempotent success: rerunning the same scoped form publish removes its aborted staged index and creates a new immutable attempt automatically, after which the whole App is retried atomically. Never work around an App failure by sequentially activating forms. `app-prepare` accepts the same overlay for a manual reviewed flow, and `app-activate <releaseId> --activate-staged-children` performs the explicit atomic activation. Calling `app-finalize` without an overlay remains a compatibility-only retrospective aggregation of already-active children. `app-rollback <releaseId> --change <change> --reason "..."` prepares the audited rollback manifest.
|
|
210
210
|
|
|
211
|
+
If exact FormRelease children were already staged for the same change before a new release baseline or lease is acquired, the CLI reuses them only after checking the server's immutable, inactive, non-aborted release, exact app/form identity and content hash, frozen schema/formType, finalized resources, parent/base revision, and current Form head. Verified children are rebound to the newly owned baseline/session; local `schemaSyncedAt` is not treated as release evidence. Missing or conflicting evidence still fails closed, and neither direct schema synchronization nor early Form activation is required.
|
|
212
|
+
|
|
211
213
|
For small live fixes, diagnosis, or AI command discovery, use first-class resource commands. They all accept `--profile`, `--app-type`, `--json`, `--json-file`, `--dry-run`, and write commands can use `--write-manifest` to avoid repo/platform drift:
|
|
212
214
|
|
|
213
215
|
```bash
|
|
@@ -536,18 +536,24 @@ function withManagedReleaseForwardedFlags(stepId, args = [], flags = {}) {
|
|
|
536
536
|
forwarded[0] === 'resource' &&
|
|
537
537
|
forwarded[1] === 'publish' &&
|
|
538
538
|
(forwarded.includes('--only') || forwarded.includes('--code'));
|
|
539
|
-
if (
|
|
540
|
-
!isExactResourcePublish ||
|
|
541
|
-
!flags['adopt-online-baseline']
|
|
542
|
-
) {
|
|
539
|
+
if (!isExactResourcePublish) {
|
|
543
540
|
return forwarded;
|
|
544
541
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
542
|
+
if (stepId === 'backend-stage' && flags['replace-manifest']) {
|
|
543
|
+
forwarded.push('--replace-manifest');
|
|
544
|
+
const replacementReason = String(flags.reason || '').trim();
|
|
545
|
+
if (replacementReason) {
|
|
546
|
+
forwarded.push('--reason', replacementReason);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (flags['adopt-online-baseline']) {
|
|
550
|
+
forwarded.push('--adopt-online-baseline');
|
|
551
|
+
const adoptionReason = String(
|
|
552
|
+
flags['adoption-reason'] || flags.reason || ''
|
|
553
|
+
).trim();
|
|
554
|
+
if (adoptionReason) {
|
|
555
|
+
forwarded.push('--adoption-reason', adoptionReason);
|
|
556
|
+
}
|
|
551
557
|
}
|
|
552
558
|
return forwarded;
|
|
553
559
|
}
|
package/lib/cli.js
CHANGED
|
@@ -303,7 +303,7 @@ Usage:
|
|
|
303
303
|
openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
304
304
|
openxiangda environment init|attach|bind|swap|policy update|list|status|diff|use [preproduction|production] [--logical-app code] [--environment target] [--json]
|
|
305
305
|
openxiangda release publish|begin|status|integration-status|renew|end [--change id] [--profile name] [--watch] [--json]
|
|
306
|
-
openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--adopt-online-baseline --adoption-reason text] [--json]
|
|
306
|
+
openxiangda release ship|candidate|deploy|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--json]
|
|
307
307
|
openxiangda task status --change <id> [--profile name] [--watch] [--json]
|
|
308
308
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
309
309
|
openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
|
|
@@ -3087,6 +3087,8 @@ const RELEASE_SHIP_SUPPORTED_FLAGS = new Set([
|
|
|
3087
3087
|
'app-type',
|
|
3088
3088
|
'since',
|
|
3089
3089
|
'expand-dependencies',
|
|
3090
|
+
'replace-manifest',
|
|
3091
|
+
'reason',
|
|
3090
3092
|
'adopt-online-baseline',
|
|
3091
3093
|
'adoption-reason',
|
|
3092
3094
|
'confirm-production',
|
|
@@ -3109,6 +3111,22 @@ function assertReleaseShipFlags(flags = {}) {
|
|
|
3109
3111
|
error.unsupportedFlags = unsupported;
|
|
3110
3112
|
throw error;
|
|
3111
3113
|
}
|
|
3114
|
+
const replacementRequested = Boolean(flags['replace-manifest']);
|
|
3115
|
+
const replacementReason = String(flags.reason || '').trim();
|
|
3116
|
+
if (!replacementRequested && replacementReason) {
|
|
3117
|
+
const error = new Error(
|
|
3118
|
+
'RELEASE_SHIP_REPLACE_MANIFEST_FLAG_REQUIRED: --reason 必须与 --replace-manifest 一起使用;命令已在任何 candidate/deployment 写入前终止'
|
|
3119
|
+
);
|
|
3120
|
+
error.code = 'RELEASE_SHIP_REPLACE_MANIFEST_FLAG_REQUIRED';
|
|
3121
|
+
throw error;
|
|
3122
|
+
}
|
|
3123
|
+
if (replacementRequested && replacementReason.length < 8) {
|
|
3124
|
+
const error = new Error(
|
|
3125
|
+
'RELEASE_SHIP_REPLACE_MANIFEST_REASON_REQUIRED: --replace-manifest 必须提供至少 8 个字符的 --reason;命令已在任何 candidate/deployment 写入前终止'
|
|
3126
|
+
);
|
|
3127
|
+
error.code = 'RELEASE_SHIP_REPLACE_MANIFEST_REASON_REQUIRED';
|
|
3128
|
+
throw error;
|
|
3129
|
+
}
|
|
3112
3130
|
const adoptionRequested = Boolean(flags['adopt-online-baseline']);
|
|
3113
3131
|
const adoptionReason = String(flags['adoption-reason'] || '').trim();
|
|
3114
3132
|
if (!adoptionRequested && adoptionReason) {
|
|
@@ -3138,7 +3156,7 @@ async function runApplicationShipCommand(
|
|
|
3138
3156
|
readStringFlag(flags, 'change') || positional[0];
|
|
3139
3157
|
if (!changeId) {
|
|
3140
3158
|
fail(
|
|
3141
|
-
'用法: openxiangda release ship --change <id> [--adopt-online-baseline --adoption-reason "..."] [--confirm-production] [--acceptance-note "..."]'
|
|
3159
|
+
'用法: openxiangda release ship --change <id> [--replace-manifest --reason "..."] [--adopt-online-baseline --adoption-reason "..."] [--confirm-production] [--acceptance-note "..."]'
|
|
3142
3160
|
);
|
|
3143
3161
|
}
|
|
3144
3162
|
const logicalApp = requireLogicalApplication(initialTarget);
|
|
@@ -3154,6 +3172,26 @@ async function runApplicationShipCommand(
|
|
|
3154
3172
|
);
|
|
3155
3173
|
const existingShip = readManagedShip(changeId);
|
|
3156
3174
|
const confirmProduction = Boolean(flags['confirm-production']);
|
|
3175
|
+
const manifestReplacement = {
|
|
3176
|
+
enabled: Boolean(flags['replace-manifest']),
|
|
3177
|
+
reason: String(flags.reason || '').trim() || null,
|
|
3178
|
+
};
|
|
3179
|
+
|
|
3180
|
+
if (existingShip?.candidateId) {
|
|
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
|
+
}
|
|
3157
3195
|
|
|
3158
3196
|
if (confirmProduction && !existingShip?.preproductionDeploymentId) {
|
|
3159
3197
|
fail(
|
|
@@ -3185,6 +3223,7 @@ async function runApplicationShipCommand(
|
|
|
3185
3223
|
candidateHash: candidate.candidateHash,
|
|
3186
3224
|
sourceRevision: candidate.sourceRevision,
|
|
3187
3225
|
candidateFile: created.candidateFile,
|
|
3226
|
+
manifestReplacement,
|
|
3188
3227
|
});
|
|
3189
3228
|
} else {
|
|
3190
3229
|
assertLocalCandidate(candidate);
|
|
@@ -3221,12 +3260,18 @@ async function runApplicationShipCommand(
|
|
|
3221
3260
|
preproductionAppReleaseId:
|
|
3222
3261
|
deployment.targetAppReleaseId || null,
|
|
3223
3262
|
preproductionStatus: deployment.status,
|
|
3263
|
+
manifestReplacement,
|
|
3224
3264
|
preparedAt:
|
|
3225
3265
|
existingShip?.preparedAt ||
|
|
3226
3266
|
new Date().toISOString(),
|
|
3227
3267
|
nextCommand:
|
|
3228
3268
|
`openxiangda release ship --change ${changeId} ` +
|
|
3229
|
-
'--confirm-production'
|
|
3269
|
+
'--confirm-production' +
|
|
3270
|
+
(manifestReplacement.enabled
|
|
3271
|
+
? ` --replace-manifest --reason ${JSON.stringify(
|
|
3272
|
+
manifestReplacement.reason
|
|
3273
|
+
)}`
|
|
3274
|
+
: ''),
|
|
3230
3275
|
});
|
|
3231
3276
|
return {
|
|
3232
3277
|
phase: 'preproduction',
|
|
@@ -3569,12 +3614,13 @@ async function release(args) {
|
|
|
3569
3614
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
3570
3615
|
print([
|
|
3571
3616
|
'用法: openxiangda release publish|begin|status|explain|integration-status|renew|end [--change id] [--profile name] [--json]',
|
|
3572
|
-
' openxiangda release ship|candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
|
|
3617
|
+
' openxiangda release ship|candidate|deploy|reconcile|test|promote|rollback [--candidate id] [--environment target] [--replace-manifest --reason text] [--adopt-online-baseline --adoption-reason text] [--confirm-production] [--json]',
|
|
3573
3618
|
' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
|
|
3574
3619
|
' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-post-commit|app-retry|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
|
|
3575
3620
|
'常用流程:',
|
|
3576
3621
|
' openxiangda release ship --change <id> --profile <name>',
|
|
3577
3622
|
' openxiangda release ship --change <id> --profile <name> --adopt-online-baseline --adoption-reason "已审计目标来自多次历史发布"',
|
|
3623
|
+
' openxiangda release ship --change <id> --profile <name> --replace-manifest --reason "已审查精确 Function manifest"',
|
|
3578
3624
|
' # 确认预发结果后;可选 --acceptance-note 留下人工验收记录:',
|
|
3579
3625
|
' openxiangda release ship --change <id> --profile <name> --confirm-production',
|
|
3580
3626
|
' openxiangda release publish --change <id> --profile <name>',
|
|
@@ -3590,6 +3636,7 @@ async function release(args) {
|
|
|
3590
3636
|
'说明:',
|
|
3591
3637
|
' - ship 是环境托管应用的两段式快车道:首次只部署预发并停止;确认预发结果后,第二次以同一 candidate 晋级正式。人工验收备注可选。',
|
|
3592
3638
|
' - 已合入主线但线上来自多次历史 lineage 的精确非删除恢复可显式使用 --adopt-online-baseline 与 --adoption-reason;仍保留冻结 Head、CAS、租约、staged children 和单次 App finalize。',
|
|
3639
|
+
' - ship 的 --replace-manifest 与 --reason 必须成对,只透传给精确 Backend Function/Automation stage;正式确认必须复用与预发完全相同的参数。',
|
|
3593
3640
|
' - ship 当前不支持 --dry-run;任何未声明参数都会在 candidate/deployment 写入前 fail-closed。需要只读检查时使用 environment status/diff 与 release status。',
|
|
3594
3641
|
' - publish 默认等待租约并按私有执行日志恢复;上次写结果不确定时必须只读核对后显式 --resume-after-review。',
|
|
3595
3642
|
' - app-head 默认只输出紧凑 head 摘要;需要完整 manifest 时显式追加 --full。',
|
|
@@ -3787,7 +3834,10 @@ async function release(args) {
|
|
|
3787
3834
|
);
|
|
3788
3835
|
}
|
|
3789
3836
|
const pendingBaseline = getStoredChangeBaseline(target);
|
|
3790
|
-
const newReleaseLifecycle = !
|
|
3837
|
+
const newReleaseLifecycle = !existing && !pendingBaseline;
|
|
3838
|
+
const reusableFormCandidates = newReleaseLifecycle
|
|
3839
|
+
? reusableStagedFormReleaseCandidates(target, changeId)
|
|
3840
|
+
: [];
|
|
3791
3841
|
const clientSessionId =
|
|
3792
3842
|
existing?.clientSessionId ||
|
|
3793
3843
|
(pendingBaseline?.changeId === changeId &&
|
|
@@ -3819,6 +3869,7 @@ async function release(args) {
|
|
|
3819
3869
|
let frozenCapture = null;
|
|
3820
3870
|
let releaseSessionFile = null;
|
|
3821
3871
|
let stagedResourcesFile = null;
|
|
3872
|
+
let reusedStagedFormReleases = [];
|
|
3822
3873
|
try {
|
|
3823
3874
|
if (flags['freeze-app-capture']) {
|
|
3824
3875
|
frozenCapture = await fetchAppReleaseCapture(config, target, flags);
|
|
@@ -3830,9 +3881,16 @@ async function release(args) {
|
|
|
3830
3881
|
frozenCapture
|
|
3831
3882
|
);
|
|
3832
3883
|
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3884
|
+
if (newReleaseLifecycle) {
|
|
3885
|
+
const initialized = await initializeNewReleaseStagedResources(
|
|
3886
|
+
config,
|
|
3887
|
+
target,
|
|
3888
|
+
changeId,
|
|
3889
|
+
reusableFormCandidates
|
|
3890
|
+
);
|
|
3891
|
+
stagedResourcesFile = initialized.file;
|
|
3892
|
+
reusedStagedFormReleases = initialized.reused;
|
|
3893
|
+
}
|
|
3836
3894
|
} catch (error) {
|
|
3837
3895
|
if (newReleaseLifecycle) {
|
|
3838
3896
|
try {
|
|
@@ -3867,6 +3925,9 @@ async function release(args) {
|
|
|
3867
3925
|
}
|
|
3868
3926
|
: {}),
|
|
3869
3927
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
3928
|
+
...(reusedStagedFormReleases.length > 0
|
|
3929
|
+
? { reusedStagedFormReleases }
|
|
3930
|
+
: {}),
|
|
3870
3931
|
};
|
|
3871
3932
|
if (flags.json) return writeJson(result);
|
|
3872
3933
|
print(
|
|
@@ -4846,14 +4907,22 @@ function writeStagedResourcesFiles(
|
|
|
4846
4907
|
return path.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
4847
4908
|
}
|
|
4848
4909
|
|
|
4849
|
-
function initializeStagedAppReleaseResources(
|
|
4910
|
+
function initializeStagedAppReleaseResources(
|
|
4911
|
+
target,
|
|
4912
|
+
changeId,
|
|
4913
|
+
resources = [],
|
|
4914
|
+
metadata = {}
|
|
4915
|
+
) {
|
|
4850
4916
|
const context = currentStagedResourcesContext(target, changeId);
|
|
4851
4917
|
return writeStagedResourcesFiles(
|
|
4852
4918
|
changeId,
|
|
4853
|
-
|
|
4919
|
+
Array.isArray(resources) && resources.length === 0
|
|
4920
|
+
? []
|
|
4921
|
+
: normalizeStagedAppReleaseResources(resources),
|
|
4854
4922
|
{
|
|
4855
4923
|
...context,
|
|
4856
4924
|
initializedAt: new Date().toISOString(),
|
|
4925
|
+
...metadata,
|
|
4857
4926
|
},
|
|
4858
4927
|
target.deploymentId
|
|
4859
4928
|
);
|
|
@@ -5036,6 +5105,278 @@ function stagedFormCode(target, resource) {
|
|
|
5036
5105
|
return formUuid;
|
|
5037
5106
|
}
|
|
5038
5107
|
|
|
5108
|
+
function reusableStagedFormReleaseCandidates(target, changeId) {
|
|
5109
|
+
const normalizedChangeId = String(changeId || '').trim();
|
|
5110
|
+
if (!normalizedChangeId) return [];
|
|
5111
|
+
const file = stagedResourcesFileForChange(
|
|
5112
|
+
normalizedChangeId,
|
|
5113
|
+
target.deploymentId
|
|
5114
|
+
);
|
|
5115
|
+
if (!fs.existsSync(file)) return [];
|
|
5116
|
+
const contextFile = stagedResourcesContextFileForChange(
|
|
5117
|
+
normalizedChangeId,
|
|
5118
|
+
target.deploymentId
|
|
5119
|
+
);
|
|
5120
|
+
if (!fs.existsSync(contextFile)) {
|
|
5121
|
+
fail(
|
|
5122
|
+
`FORM_RELEASE_REUSE_CONTEXT_REQUIRED: change ${normalizedChangeId} 的 staged FormRelease 缺少 context,拒绝重挂接`
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
const context = JSON.parse(fs.readFileSync(contextFile, 'utf8'));
|
|
5126
|
+
const expected = {
|
|
5127
|
+
contractVersion: 'staged_resources_context_v1',
|
|
5128
|
+
appType: target.appType,
|
|
5129
|
+
profile: target.profileName,
|
|
5130
|
+
changeId: normalizedChangeId,
|
|
5131
|
+
deploymentId: target.deploymentId || null,
|
|
5132
|
+
};
|
|
5133
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
5134
|
+
if ((context?.[field] ?? null) !== (value ?? null)) {
|
|
5135
|
+
fail(
|
|
5136
|
+
`FORM_RELEASE_REUSE_CONTEXT_MISMATCH: ${field} 不属于当前 app/profile/change/deployment,拒绝重挂接 staged FormRelease`
|
|
5137
|
+
);
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
5141
|
+
const resources = Array.isArray(parsed) && parsed.length === 0
|
|
5142
|
+
? []
|
|
5143
|
+
: normalizeStagedAppReleaseResources(parsed);
|
|
5144
|
+
let scopedForms = null;
|
|
5145
|
+
try {
|
|
5146
|
+
scopedForms = new Set(
|
|
5147
|
+
getSddChangeScope({
|
|
5148
|
+
cwd: process.cwd(),
|
|
5149
|
+
configText: readWorkspaceConfigText(),
|
|
5150
|
+
changeId: normalizedChangeId,
|
|
5151
|
+
}).targets.forms || []
|
|
5152
|
+
);
|
|
5153
|
+
} catch (error) {
|
|
5154
|
+
// A direct release may use --change without a local SDD record. The
|
|
5155
|
+
// exact local context still supplies the app/profile/change scope, while
|
|
5156
|
+
// every candidate is independently verified against immutable server data.
|
|
5157
|
+
if (!/SDD 未初始化|change 不存在|ENOENT/i.test(String(error?.message || ''))) {
|
|
5158
|
+
throw error;
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
return resources
|
|
5162
|
+
.filter(resource => resource.kind === 'FormRelease')
|
|
5163
|
+
.map(resource => {
|
|
5164
|
+
const formCode = stagedFormCode(target, resource);
|
|
5165
|
+
if (
|
|
5166
|
+
scopedForms &&
|
|
5167
|
+
(!formCode || !scopedForms.has(formCode))
|
|
5168
|
+
) {
|
|
5169
|
+
fail(
|
|
5170
|
+
`FORM_RELEASE_REUSE_SCOPE_MISMATCH: ${formCode || resource.identity?.formUuid || '(unknown)'} 不在 change ${normalizedChangeId} 的精确 Form 范围内`
|
|
5171
|
+
);
|
|
5172
|
+
}
|
|
5173
|
+
return { resource, formCode };
|
|
5174
|
+
});
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
function nullableReleaseId(value) {
|
|
5178
|
+
const normalized = String(value || '').trim();
|
|
5179
|
+
return normalized || null;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
async function verifyReusableStagedFormRelease(
|
|
5183
|
+
config,
|
|
5184
|
+
target,
|
|
5185
|
+
candidate
|
|
5186
|
+
) {
|
|
5187
|
+
const resource = candidate.resource;
|
|
5188
|
+
const formCode = String(candidate.formCode || '').trim();
|
|
5189
|
+
const releaseId = String(resource.identity?.releaseId || '').trim();
|
|
5190
|
+
const formUuid = String(resource.identity?.formUuid || '').trim();
|
|
5191
|
+
const boundFormUuid = String(
|
|
5192
|
+
target.bound.resources?.forms?.[formCode]?.formUuid ||
|
|
5193
|
+
target.bound.resources?.formSettings?.[formCode]?.formUuid ||
|
|
5194
|
+
''
|
|
5195
|
+
).trim();
|
|
5196
|
+
if (!formCode || !boundFormUuid || boundFormUuid !== formUuid) {
|
|
5197
|
+
fail(
|
|
5198
|
+
`FORM_RELEASE_REUSE_BINDING_MISMATCH: ${formCode || formUuid || '(unknown)'} 的本地 binding 与 staged FormRelease identity 不一致`
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}`;
|
|
5202
|
+
const [detail, head] = await Promise.all([
|
|
5203
|
+
requestWithAuth(
|
|
5204
|
+
config,
|
|
5205
|
+
target.profileName,
|
|
5206
|
+
`${basePath}/releases/${encodeURIComponent(releaseId)}`
|
|
5207
|
+
),
|
|
5208
|
+
requestWithAuth(
|
|
5209
|
+
config,
|
|
5210
|
+
target.profileName,
|
|
5211
|
+
`${basePath}/releases/head`
|
|
5212
|
+
),
|
|
5213
|
+
]);
|
|
5214
|
+
if (
|
|
5215
|
+
detail?.immutable !== true ||
|
|
5216
|
+
detail?.active === true ||
|
|
5217
|
+
detail?.aborted === true ||
|
|
5218
|
+
(Array.isArray(detail?.journal) ? detail.journal : []).some(
|
|
5219
|
+
entry => entry?.action === 'abort'
|
|
5220
|
+
)
|
|
5221
|
+
) {
|
|
5222
|
+
fail(
|
|
5223
|
+
`FORM_RELEASE_REUSE_STATE_INVALID: ${formCode} 的 FormRelease ${releaseId} 必须 immutable=true、active=false、aborted=false`
|
|
5224
|
+
);
|
|
5225
|
+
}
|
|
5226
|
+
if (
|
|
5227
|
+
String(detail?.id || '').trim() !== releaseId ||
|
|
5228
|
+
String(detail?.formUuid || '').trim() !== formUuid
|
|
5229
|
+
) {
|
|
5230
|
+
fail(
|
|
5231
|
+
`FORM_RELEASE_REUSE_IDENTITY_MISMATCH: ${formCode} 的 releaseId/formUuid 与服务端冻结证据不一致`
|
|
5232
|
+
);
|
|
5233
|
+
}
|
|
5234
|
+
const contentHash = String(detail?.contentHash || '')
|
|
5235
|
+
.trim()
|
|
5236
|
+
.toLowerCase();
|
|
5237
|
+
if (
|
|
5238
|
+
!/^[a-f0-9]{64}$/.test(contentHash) ||
|
|
5239
|
+
contentHash !== String(resource.hash || '').trim().toLowerCase()
|
|
5240
|
+
) {
|
|
5241
|
+
fail(
|
|
5242
|
+
`FORM_RELEASE_REUSE_HASH_MISMATCH: ${formCode} 的 staged hash 与 immutable FormRelease 不一致`
|
|
5243
|
+
);
|
|
5244
|
+
}
|
|
5245
|
+
const parentReleaseId = nullableReleaseId(detail?.parentReleaseId);
|
|
5246
|
+
const baseRevision = Number(detail?.baseRevision);
|
|
5247
|
+
if (
|
|
5248
|
+
nullableReleaseId(resource.revision?.parentReleaseId) !==
|
|
5249
|
+
parentReleaseId ||
|
|
5250
|
+
Number(resource.revision?.baseRevision) !== baseRevision
|
|
5251
|
+
) {
|
|
5252
|
+
fail(
|
|
5253
|
+
`FORM_RELEASE_REUSE_REVISION_MISMATCH: ${formCode} 的 staged parent/baseRevision 与 immutable FormRelease 不一致`
|
|
5254
|
+
);
|
|
5255
|
+
}
|
|
5256
|
+
const activeHead = head?.activeFormReleaseHead || {};
|
|
5257
|
+
const currentRevision = Number(head?.revision ?? activeHead.revision);
|
|
5258
|
+
if (
|
|
5259
|
+
nullableReleaseId(activeHead.releaseId) !== parentReleaseId ||
|
|
5260
|
+
!Number.isSafeInteger(currentRevision) ||
|
|
5261
|
+
currentRevision !== baseRevision
|
|
5262
|
+
) {
|
|
5263
|
+
fail(
|
|
5264
|
+
`FORM_RELEASE_REUSE_PARENT_CONFLICT: ${formCode} 的 active head/revision 已变化,拒绝重挂接 staged FormRelease`
|
|
5265
|
+
);
|
|
5266
|
+
}
|
|
5267
|
+
if (
|
|
5268
|
+
!Array.isArray(detail?.resources) ||
|
|
5269
|
+
detail.resources.length === 0 ||
|
|
5270
|
+
detail.resources.some(item => !item?.finalizedAt)
|
|
5271
|
+
) {
|
|
5272
|
+
fail(
|
|
5273
|
+
`FORM_RELEASE_REUSE_RESOURCES_INVALID: ${formCode} 的 FormRelease 资源尚未全部 finalized`
|
|
5274
|
+
);
|
|
5275
|
+
}
|
|
5276
|
+
const frozenForm = detail?.snapshotJson?.form;
|
|
5277
|
+
if (!frozenForm || frozenForm.schema === undefined || !frozenForm.formType) {
|
|
5278
|
+
fail(
|
|
5279
|
+
`FORM_RELEASE_REUSE_SCHEMA_MISSING: ${formCode} 的 FormRelease 缺少冻结 schema/formType`
|
|
5280
|
+
);
|
|
5281
|
+
}
|
|
5282
|
+
let frozenSchema = frozenForm.schema;
|
|
5283
|
+
if (typeof frozenSchema === 'string') {
|
|
5284
|
+
try {
|
|
5285
|
+
frozenSchema = JSON.parse(frozenSchema);
|
|
5286
|
+
} catch {
|
|
5287
|
+
fail(
|
|
5288
|
+
`FORM_RELEASE_REUSE_SCHEMA_INVALID: ${formCode} 的 FormRelease 冻结 schema 不是合法 JSON`
|
|
5289
|
+
);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
if (!frozenSchema || typeof frozenSchema !== 'object') {
|
|
5293
|
+
fail(
|
|
5294
|
+
`FORM_RELEASE_REUSE_SCHEMA_INVALID: ${formCode} 的 FormRelease 冻结 schema 无效`
|
|
5295
|
+
);
|
|
5296
|
+
}
|
|
5297
|
+
return {
|
|
5298
|
+
resource: {
|
|
5299
|
+
kind: 'FormRelease',
|
|
5300
|
+
identity: { releaseId, formUuid },
|
|
5301
|
+
action: resource.action || 'update',
|
|
5302
|
+
hash: contentHash,
|
|
5303
|
+
revision: { parentReleaseId, baseRevision },
|
|
5304
|
+
metadata: {
|
|
5305
|
+
...(resource.metadata || {}),
|
|
5306
|
+
formCode,
|
|
5307
|
+
releaseStatus: 'staged',
|
|
5308
|
+
},
|
|
5309
|
+
},
|
|
5310
|
+
formCode,
|
|
5311
|
+
formUuid,
|
|
5312
|
+
releaseId,
|
|
5313
|
+
contentHash,
|
|
5314
|
+
baseRevision,
|
|
5315
|
+
parentReleaseId,
|
|
5316
|
+
formSnapshot: {
|
|
5317
|
+
...clonePlainJson(frozenForm),
|
|
5318
|
+
schema: clonePlainJson(frozenSchema),
|
|
5319
|
+
},
|
|
5320
|
+
formType: frozenForm.formType,
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
async function resolveReusableStagedFormReleaseOverlays(
|
|
5325
|
+
config,
|
|
5326
|
+
target,
|
|
5327
|
+
changeId,
|
|
5328
|
+
options = {}
|
|
5329
|
+
) {
|
|
5330
|
+
const requested = new Set(
|
|
5331
|
+
(options.formCodes || [])
|
|
5332
|
+
.map(value => String(value || '').trim())
|
|
5333
|
+
.filter(Boolean)
|
|
5334
|
+
);
|
|
5335
|
+
const candidates = reusableStagedFormReleaseCandidates(target, changeId)
|
|
5336
|
+
.filter(candidate => requested.size === 0 || requested.has(candidate.formCode));
|
|
5337
|
+
const verified = [];
|
|
5338
|
+
for (const candidate of candidates) {
|
|
5339
|
+
verified.push(
|
|
5340
|
+
await verifyReusableStagedFormRelease(config, target, candidate)
|
|
5341
|
+
);
|
|
5342
|
+
}
|
|
5343
|
+
return new Map(verified.map(item => [item.formCode, item]));
|
|
5344
|
+
}
|
|
5345
|
+
|
|
5346
|
+
async function initializeNewReleaseStagedResources(
|
|
5347
|
+
config,
|
|
5348
|
+
target,
|
|
5349
|
+
changeId,
|
|
5350
|
+
candidates
|
|
5351
|
+
) {
|
|
5352
|
+
const verified = [];
|
|
5353
|
+
for (const candidate of candidates || []) {
|
|
5354
|
+
verified.push(
|
|
5355
|
+
await verifyReusableStagedFormRelease(config, target, candidate)
|
|
5356
|
+
);
|
|
5357
|
+
}
|
|
5358
|
+
const reused = verified.map(item => ({
|
|
5359
|
+
formCode: item.formCode,
|
|
5360
|
+
formUuid: item.formUuid,
|
|
5361
|
+
releaseId: item.releaseId,
|
|
5362
|
+
contentHash: item.contentHash,
|
|
5363
|
+
baseRevision: item.baseRevision,
|
|
5364
|
+
parentReleaseId: item.parentReleaseId,
|
|
5365
|
+
}));
|
|
5366
|
+
const file = initializeStagedAppReleaseResources(
|
|
5367
|
+
target,
|
|
5368
|
+
changeId,
|
|
5369
|
+
verified.map(item => item.resource),
|
|
5370
|
+
reused.length > 0
|
|
5371
|
+
? {
|
|
5372
|
+
reusedStagedFormReleases: reused,
|
|
5373
|
+
reusedAt: new Date().toISOString(),
|
|
5374
|
+
}
|
|
5375
|
+
: {}
|
|
5376
|
+
);
|
|
5377
|
+
return { file, reused };
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5039
5380
|
function assertStagedAppReleaseScope(target, flags, resources) {
|
|
5040
5381
|
const changeId = readStringFlag(flags, 'change');
|
|
5041
5382
|
if (!changeId) {
|
|
@@ -6036,6 +6377,15 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
6036
6377
|
const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
|
|
6037
6378
|
const changeId = readStringFlag(flags, 'change');
|
|
6038
6379
|
const storedLease = getStoredPublishLease(target);
|
|
6380
|
+
const baselineAtStart = changeId
|
|
6381
|
+
? getStoredChangeBaseline(target)
|
|
6382
|
+
: null;
|
|
6383
|
+
const newReleaseLifecycle = Boolean(
|
|
6384
|
+
changeId && !getUsableStoredPublishLease(target) && !baselineAtStart
|
|
6385
|
+
);
|
|
6386
|
+
const reusableFormCandidates = newReleaseLifecycle
|
|
6387
|
+
? reusableStagedFormReleaseCandidates(target, changeId)
|
|
6388
|
+
: [];
|
|
6039
6389
|
let acquiredByCommand = false;
|
|
6040
6390
|
assertOrClaimWorktreeOwner({
|
|
6041
6391
|
cwd: process.cwd(),
|
|
@@ -6105,7 +6455,41 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
6105
6455
|
clearRejectedPendingBaseline(target, baseline, error);
|
|
6106
6456
|
throw error;
|
|
6107
6457
|
}
|
|
6108
|
-
finalizeReleaseBaseline(target, baseline);
|
|
6458
|
+
const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
|
|
6459
|
+
if (newReleaseLifecycle) {
|
|
6460
|
+
try {
|
|
6461
|
+
await initializeNewReleaseStagedResources(
|
|
6462
|
+
config,
|
|
6463
|
+
target,
|
|
6464
|
+
changeId,
|
|
6465
|
+
reusableFormCandidates
|
|
6466
|
+
);
|
|
6467
|
+
} catch (error) {
|
|
6468
|
+
try {
|
|
6469
|
+
await requestWithAuth(
|
|
6470
|
+
config,
|
|
6471
|
+
target.profileName,
|
|
6472
|
+
publishLeaseApiPath(
|
|
6473
|
+
target,
|
|
6474
|
+
`/${encodeURIComponent(lease.leaseId)}/release`
|
|
6475
|
+
),
|
|
6476
|
+
{
|
|
6477
|
+
method: 'POST',
|
|
6478
|
+
body: { completion: 'local-prepare-failed' },
|
|
6479
|
+
}
|
|
6480
|
+
);
|
|
6481
|
+
} catch {
|
|
6482
|
+
// Keep the staged child verification failure as the primary error.
|
|
6483
|
+
}
|
|
6484
|
+
clearPublishLease(target, lease.leaseId);
|
|
6485
|
+
clearChangeBaseline(
|
|
6486
|
+
target,
|
|
6487
|
+
finalizedBaseline?.baselineId || finalizedBaseline?.id
|
|
6488
|
+
);
|
|
6489
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
6490
|
+
throw error;
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6109
6493
|
}
|
|
6110
6494
|
if (!lease) return null;
|
|
6111
6495
|
if (changeId && lease.changeId !== changeId) {
|
|
@@ -12588,9 +12972,34 @@ async function resource(args) {
|
|
|
12588
12972
|
|
|
12589
12973
|
const validation = validateWorkspaceResources(manifest);
|
|
12590
12974
|
let target = null;
|
|
12975
|
+
let stagedFormBindingOverlays = new Map();
|
|
12591
12976
|
if (!isManifestEmpty(manifest)) {
|
|
12592
12977
|
target = getWorkspaceTarget(config, profileName, flags);
|
|
12593
|
-
|
|
12978
|
+
const workflowFormCodes = unique(
|
|
12979
|
+
(manifest.workflows || [])
|
|
12980
|
+
.map(item => item.formCode || item.form)
|
|
12981
|
+
.filter(Boolean)
|
|
12982
|
+
);
|
|
12983
|
+
const stagedChangeId =
|
|
12984
|
+
readStringFlag(flags, 'change') ||
|
|
12985
|
+
getStoredChangeBaseline(target, { access: 'reconciliation-read' })
|
|
12986
|
+
?.changeId ||
|
|
12987
|
+
'';
|
|
12988
|
+
if (workflowFormCodes.length > 0 && stagedChangeId) {
|
|
12989
|
+
stagedFormBindingOverlays =
|
|
12990
|
+
await resolveReusableStagedFormReleaseOverlays(
|
|
12991
|
+
config,
|
|
12992
|
+
target,
|
|
12993
|
+
stagedChangeId,
|
|
12994
|
+
{ formCodes: workflowFormCodes }
|
|
12995
|
+
);
|
|
12996
|
+
}
|
|
12997
|
+
validateWorkspaceResourceBindings(
|
|
12998
|
+
manifest,
|
|
12999
|
+
target.bound,
|
|
13000
|
+
validation,
|
|
13001
|
+
stagedFormBindingOverlays
|
|
13002
|
+
);
|
|
12594
13003
|
}
|
|
12595
13004
|
await validateCompiledWorkflowResources(manifest, validation);
|
|
12596
13005
|
if (subcommand === 'validate') {
|
|
@@ -16076,7 +16485,12 @@ function validateWorkspaceResources(manifest) {
|
|
|
16076
16485
|
};
|
|
16077
16486
|
}
|
|
16078
16487
|
|
|
16079
|
-
function validateWorkspaceResourceBindings(
|
|
16488
|
+
function validateWorkspaceResourceBindings(
|
|
16489
|
+
manifest,
|
|
16490
|
+
bound,
|
|
16491
|
+
validation,
|
|
16492
|
+
stagedFormBindingOverlays = new Map()
|
|
16493
|
+
) {
|
|
16080
16494
|
for (const item of manifest.workflows || []) {
|
|
16081
16495
|
if (!item.formCode && !item.form) continue;
|
|
16082
16496
|
const formCode = item.formCode || item.form;
|
|
@@ -16089,14 +16503,25 @@ function validateWorkspaceResourceBindings(manifest, bound, validation) {
|
|
|
16089
16503
|
continue;
|
|
16090
16504
|
}
|
|
16091
16505
|
const localSchemaPath = path.join(process.cwd(), 'src', 'forms', formCode, 'schema.ts');
|
|
16092
|
-
|
|
16506
|
+
const stagedOverlay = stagedFormBindingOverlays.get(formCode);
|
|
16507
|
+
if (
|
|
16508
|
+
fs.existsSync(localSchemaPath) &&
|
|
16509
|
+
!formBinding.schemaSyncedAt &&
|
|
16510
|
+
!stagedOverlay
|
|
16511
|
+
) {
|
|
16093
16512
|
validation.errors.push(
|
|
16094
16513
|
`${resourceLabel('workflow', item)}: formCode ${formCode} 已绑定但本地 schema 尚未同步。请先运行 openxiangda workspace publish --profile <name> --form ${formCode}`
|
|
16095
16514
|
);
|
|
16096
16515
|
}
|
|
16097
|
-
if (
|
|
16516
|
+
if (stagedOverlay) {
|
|
16517
|
+
validation.warnings.push(
|
|
16518
|
+
`${resourceLabel('workflow', item)}: formCode ${formCode} 使用已验证 staged FormRelease ${stagedOverlay.releaseId} 的冻结 schema/formType`
|
|
16519
|
+
);
|
|
16520
|
+
}
|
|
16521
|
+
const effectiveFormType = stagedOverlay?.formType || formBinding.formType;
|
|
16522
|
+
if (effectiveFormType && effectiveFormType !== 'process') {
|
|
16098
16523
|
validation.errors.push(
|
|
16099
|
-
`${resourceLabel('workflow', item)}: formCode ${formCode} 当前 formType=${
|
|
16524
|
+
`${resourceLabel('workflow', item)}: formCode ${formCode} 当前 formType=${effectiveFormType},流程表单需要 process`
|
|
16100
16525
|
);
|
|
16101
16526
|
}
|
|
16102
16527
|
}
|
|
@@ -17350,8 +17775,12 @@ async function buildResourcePlan(
|
|
|
17350
17775
|
await prepareManifestJsCodeBundlesForPlan(manifest);
|
|
17351
17776
|
const stagedLocalFormSnapshots =
|
|
17352
17777
|
await resolveStagedLocalFormContractSnapshots(
|
|
17778
|
+
config,
|
|
17353
17779
|
target,
|
|
17354
|
-
options.flags || {}
|
|
17780
|
+
options.flags || {},
|
|
17781
|
+
(manifest.workflows || [])
|
|
17782
|
+
.map(item => item.formCode || item.form)
|
|
17783
|
+
.filter(Boolean)
|
|
17355
17784
|
);
|
|
17356
17785
|
const [existing, formFieldContracts] = await Promise.all([
|
|
17357
17786
|
fetchExistingResourceMaps(config, target, manifest),
|
|
@@ -17855,6 +18284,66 @@ function buildAutomationBackendReleaseSources(definition, label) {
|
|
|
17855
18284
|
return sources;
|
|
17856
18285
|
}
|
|
17857
18286
|
|
|
18287
|
+
function isCompleteDeclarativeAutomationDefinition(definition) {
|
|
18288
|
+
if (
|
|
18289
|
+
!definition ||
|
|
18290
|
+
typeof definition !== 'object' ||
|
|
18291
|
+
Array.isArray(definition) ||
|
|
18292
|
+
definition.version !== 'v3' ||
|
|
18293
|
+
!Array.isArray(definition.nodes) ||
|
|
18294
|
+
definition.nodes.length === 0 ||
|
|
18295
|
+
!Array.isArray(definition.edges)
|
|
18296
|
+
) {
|
|
18297
|
+
return false;
|
|
18298
|
+
}
|
|
18299
|
+
const nodeIds = new Set();
|
|
18300
|
+
let startCount = 0;
|
|
18301
|
+
for (const node of definition.nodes) {
|
|
18302
|
+
if (
|
|
18303
|
+
!node ||
|
|
18304
|
+
typeof node !== 'object' ||
|
|
18305
|
+
!String(node.id || '').trim() ||
|
|
18306
|
+
!String(node.type || '').trim() ||
|
|
18307
|
+
!node.data ||
|
|
18308
|
+
typeof node.data !== 'object' ||
|
|
18309
|
+
Array.isArray(node.data)
|
|
18310
|
+
) {
|
|
18311
|
+
return false;
|
|
18312
|
+
}
|
|
18313
|
+
const nodeId = String(node.id);
|
|
18314
|
+
if (nodeIds.has(nodeId)) return false;
|
|
18315
|
+
nodeIds.add(nodeId);
|
|
18316
|
+
if (node.type === 'start') startCount += 1;
|
|
18317
|
+
}
|
|
18318
|
+
if (startCount !== 1) return false;
|
|
18319
|
+
return definition.edges.every(
|
|
18320
|
+
edge =>
|
|
18321
|
+
edge &&
|
|
18322
|
+
typeof edge === 'object' &&
|
|
18323
|
+
String(edge.id || '').trim() &&
|
|
18324
|
+
nodeIds.has(String(edge.source || '')) &&
|
|
18325
|
+
nodeIds.has(String(edge.target || ''))
|
|
18326
|
+
);
|
|
18327
|
+
}
|
|
18328
|
+
|
|
18329
|
+
function resolveAutomationCreateBackendReleaseMode(candidate, definition) {
|
|
18330
|
+
if (
|
|
18331
|
+
candidate.operation !== 'create' ||
|
|
18332
|
+
candidate.mode === 'manifest_replacement'
|
|
18333
|
+
) {
|
|
18334
|
+
return candidate.mode;
|
|
18335
|
+
}
|
|
18336
|
+
if (buildAutomationSourcePatchSources({}, definition).length > 0) {
|
|
18337
|
+
return 'source_only';
|
|
18338
|
+
}
|
|
18339
|
+
if (isCompleteDeclarativeAutomationDefinition(definition)) {
|
|
18340
|
+
return 'manifest_replacement';
|
|
18341
|
+
}
|
|
18342
|
+
fail(
|
|
18343
|
+
`Automation:${candidate.code} 新建资源既不包含可发布的源码快照,也不包含完整的 definitionJson.version=v3 声明式定义`
|
|
18344
|
+
);
|
|
18345
|
+
}
|
|
18346
|
+
|
|
17858
18347
|
async function buildBackendReleaseResourceInput(
|
|
17859
18348
|
config,
|
|
17860
18349
|
target,
|
|
@@ -17945,21 +18434,37 @@ async function buildBackendReleaseResourceInput(
|
|
|
17945
18434
|
true,
|
|
17946
18435
|
uploadCache
|
|
17947
18436
|
));
|
|
18437
|
+
const mode = resolveAutomationCreateBackendReleaseMode(
|
|
18438
|
+
candidate,
|
|
18439
|
+
definitionJson
|
|
18440
|
+
);
|
|
18441
|
+
candidate.mode = mode;
|
|
17948
18442
|
const common = {
|
|
17949
18443
|
kind: 'Automation',
|
|
17950
18444
|
code: candidate.code,
|
|
17951
|
-
mode
|
|
18445
|
+
mode,
|
|
17952
18446
|
operation: candidate.operation,
|
|
17953
18447
|
};
|
|
17954
|
-
if (
|
|
18448
|
+
if (mode === 'manifest_replacement') {
|
|
17955
18449
|
return {
|
|
17956
18450
|
...common,
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
18451
|
+
...(candidate.operation === 'create'
|
|
18452
|
+
? {
|
|
18453
|
+
create: buildBackendReleaseAutomationMetadata(
|
|
18454
|
+
target,
|
|
18455
|
+
candidate.item,
|
|
18456
|
+
definitionJson,
|
|
18457
|
+
viewJson
|
|
18458
|
+
),
|
|
18459
|
+
}
|
|
18460
|
+
: {
|
|
18461
|
+
replacement: buildBackendReleaseAutomationMetadata(
|
|
18462
|
+
target,
|
|
18463
|
+
candidate.item,
|
|
18464
|
+
definitionJson,
|
|
18465
|
+
viewJson
|
|
18466
|
+
),
|
|
18467
|
+
}),
|
|
17963
18468
|
};
|
|
17964
18469
|
}
|
|
17965
18470
|
return {
|
|
@@ -18358,9 +18863,6 @@ async function publishBackendReleaseResources(
|
|
|
18358
18863
|
`${candidate.kind === 'Function' ? 'function' : 'automation'}:${candidate.code}`
|
|
18359
18864
|
)
|
|
18360
18865
|
.sort();
|
|
18361
|
-
const resourceModes = unique(candidates.map(candidate => candidate.mode));
|
|
18362
|
-
const releaseMode =
|
|
18363
|
-
resourceModes.length > 1 ? 'mixed' : resourceModes[0] || 'source_only';
|
|
18364
18866
|
const replacementReason = String(
|
|
18365
18867
|
options.publishLeaseFlags?.reason ||
|
|
18366
18868
|
options.publishLeaseFlags?.['replace-reason'] ||
|
|
@@ -18418,6 +18920,14 @@ async function publishBackendReleaseResources(
|
|
|
18418
18920
|
uploadCache
|
|
18419
18921
|
)
|
|
18420
18922
|
);
|
|
18923
|
+
const resourceModes = unique(resources.map(resource => resource.mode));
|
|
18924
|
+
const releaseMode =
|
|
18925
|
+
resourceModes.length > 1 ? 'mixed' : resourceModes[0] || 'source_only';
|
|
18926
|
+
const requiresReplacementReason = resources.some(
|
|
18927
|
+
resource =>
|
|
18928
|
+
resource.mode === 'manifest_replacement' &&
|
|
18929
|
+
resource.operation === 'update'
|
|
18930
|
+
);
|
|
18421
18931
|
const prepared = await requestWithAuth(
|
|
18422
18932
|
config,
|
|
18423
18933
|
target.profileName,
|
|
@@ -18429,7 +18939,7 @@ async function publishBackendReleaseResources(
|
|
|
18429
18939
|
...(protocolVersion ? { protocolVersion } : {}),
|
|
18430
18940
|
parentReleaseId: head?.activeBackendReleaseId || null,
|
|
18431
18941
|
mode: releaseMode,
|
|
18432
|
-
...(
|
|
18942
|
+
...(requiresReplacementReason
|
|
18433
18943
|
? { replacementReason }
|
|
18434
18944
|
: {}),
|
|
18435
18945
|
sourceRepositoryId: releaseContext.sourceRepositoryId,
|
|
@@ -26727,32 +27237,38 @@ async function runWorkspaceChildCommand(command, args, options = {}) {
|
|
|
26727
27237
|
}
|
|
26728
27238
|
|
|
26729
27239
|
async function resolveStagedLocalFormContractSnapshots(
|
|
27240
|
+
config,
|
|
26730
27241
|
target,
|
|
26731
|
-
flags = {}
|
|
27242
|
+
flags = {},
|
|
27243
|
+
implicitFormCodes = []
|
|
26732
27244
|
) {
|
|
26733
|
-
const
|
|
27245
|
+
const explicitFormCodes = unique(
|
|
26734
27246
|
String(flags['staged-form-contracts'] || '')
|
|
26735
27247
|
.split(',')
|
|
26736
27248
|
.map(value => value.trim())
|
|
26737
27249
|
.filter(Boolean)
|
|
26738
27250
|
).sort();
|
|
27251
|
+
const formCodes = unique([
|
|
27252
|
+
...explicitFormCodes,
|
|
27253
|
+
...(implicitFormCodes || [])
|
|
27254
|
+
.map(value => String(value || '').trim())
|
|
27255
|
+
.filter(Boolean),
|
|
27256
|
+
]).sort();
|
|
26739
27257
|
if (formCodes.length === 0) return new Map();
|
|
26740
27258
|
const changeId = readStringFlag(flags, 'change');
|
|
26741
27259
|
if (!changeId) {
|
|
27260
|
+
if (explicitFormCodes.length === 0) return new Map();
|
|
26742
27261
|
fail(
|
|
26743
27262
|
'FORM_FIELD_STAGED_CONTRACT_CHANGE_REQUIRED: --staged-form-contracts 必须与 --change 一起使用'
|
|
26744
27263
|
);
|
|
26745
27264
|
}
|
|
26746
|
-
const
|
|
26747
|
-
|
|
26748
|
-
|
|
26749
|
-
|
|
26750
|
-
|
|
26751
|
-
|
|
26752
|
-
|
|
26753
|
-
JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
26754
|
-
)
|
|
26755
|
-
: [];
|
|
27265
|
+
const stagedOverlays =
|
|
27266
|
+
await resolveReusableStagedFormReleaseOverlays(
|
|
27267
|
+
config,
|
|
27268
|
+
target,
|
|
27269
|
+
changeId,
|
|
27270
|
+
{ formCodes }
|
|
27271
|
+
);
|
|
26756
27272
|
const snapshots = new Map();
|
|
26757
27273
|
for (const formCode of formCodes) {
|
|
26758
27274
|
const formUuid = resolveManifestFormUuid(
|
|
@@ -26760,29 +27276,15 @@ async function resolveStagedLocalFormContractSnapshots(
|
|
|
26760
27276
|
{ formCode },
|
|
26761
27277
|
{ fallbackToCode: false }
|
|
26762
27278
|
);
|
|
26763
|
-
const staged =
|
|
26764
|
-
|
|
26765
|
-
resource.kind === 'FormRelease' &&
|
|
26766
|
-
String(resource.identity?.formUuid || '').trim() === formUuid
|
|
26767
|
-
);
|
|
26768
|
-
if (!staged) {
|
|
27279
|
+
const staged = stagedOverlays.get(formCode);
|
|
27280
|
+
if (!staged || staged.formUuid !== formUuid) {
|
|
26769
27281
|
fail(
|
|
26770
27282
|
`FORM_FIELD_STAGED_CONTRACT_REQUIRED: ${formCode} 尚未在 change ${changeId} 的当前 deployment 中获得 verified FormRelease`
|
|
26771
27283
|
);
|
|
26772
27284
|
}
|
|
26773
|
-
const localSchema = await loadLocalFormSchema(
|
|
26774
|
-
process.cwd(),
|
|
26775
|
-
formCode
|
|
26776
|
-
);
|
|
26777
|
-
if (!localSchema) {
|
|
26778
|
-
fail(
|
|
26779
|
-
`FORM_FIELD_STAGED_CONTRACT_SCHEMA_MISSING: ${formCode} 缺少本地 schema`
|
|
26780
|
-
);
|
|
26781
|
-
}
|
|
26782
|
-
const schema = JSON.parse(localSchema.schema);
|
|
26783
27285
|
snapshots.set(
|
|
26784
27286
|
formUuid,
|
|
26785
|
-
collectFormSnapshotFieldIds({ form:
|
|
27287
|
+
collectFormSnapshotFieldIds({ form: staged.formSnapshot })
|
|
26786
27288
|
);
|
|
26787
27289
|
}
|
|
26788
27290
|
return snapshots;
|
|
@@ -108,6 +108,8 @@ For a workspace registered by `openxiangda environment init` or connected to an
|
|
|
108
108
|
|
|
109
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; 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
|
+
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
|
+
|
|
111
113
|
`resource plan` and publish dry-runs are strictly GET/HEAD-only. `READ_ONLY_AUTH_REQUIRED` means the access token expired; run `openxiangda auth refresh --profile <name>` or log in again before retrying. Never add an automatic refresh POST inside a plan.
|
|
112
114
|
|
|
113
115
|
`release publish` is the default promotion entrypoint only for legacy unmanaged workspaces. It verifies without rewriting reviewed `change.json`/`release.json`, waits for the app lease, freezes the App capture after ownership is acquired, executes deterministic exact staged steps, resumes from `.openxiangda/releases/<change>/execution.json`, atomically finalizes, verifies mainline integration, and releases the lease. Environment-managed applications use the two-phase `release ship`; candidate/deploy/test/promote, `release begin`, and child commands remain recovery/diagnostic primitives.
|
|
@@ -124,7 +126,9 @@ Because promotion begins from the already-pushed authoritative mainline, `releas
|
|
|
124
126
|
|
|
125
127
|
For a whole-app release, keep changed Runtime/Page/Backend/Form children staged and run `openxiangda release app-finalize --change <change> --staged-resources-json <JSON|file> --profile <name>`. The JSON contains only changed immutable child entries; the CLI overlays Runtime/Page/Backend by singleton kind and FormRelease by `formUuid` onto one authoritative read-only capture, preserving every unmodified active child. It then performs `prepare -> verify -> activateStagedChildren=true`, switching child heads and the App head atomically (`atomic_staged_children_v1`, or `atomic_staged_children_v2` when a Backend Release v2 child is present). Never refresh or retry after a conflict. The flow requires the owned stored lease/change baseline and carries the same client session and Git lineage on every write. Omitting the overlay is compatibility-only retrospective aggregation of already-active children.
|
|
126
128
|
|
|
127
|
-
When
|
|
129
|
+
When the same change already has staged FormRelease children before a fresh baseline/lease is acquired, keep them staged. The CLI may rebind each child into the new session only after server verification of immutable/inactive/non-aborted state, exact app/form identity and content hash, frozen schema/formType, finalized resources, parent/base revision, and current Form head. `schemaSyncedAt` is local cache metadata, not release evidence. Do not direct-publish or activate a Form to bypass Workflow validation; missing or conflicting staged evidence must fail closed.
|
|
130
|
+
|
|
131
|
+
When a Function or Automation is selected because its TypeScript source changed, publishing is source-only by default. Backend Release v2 accepts source-backed create, source-free declarative Automation manifest create, source-only update, and manifest replacement update in one immutable child and one database transaction, so a stale member produces zero resource writes and noops do not advance versions/timestamps. 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. `--stage-only` is fail-closed: every selected mutation must enter that child, and a missing/incompatible Backend Release API never falls back to direct writes. Online bindings, input/output contracts, metadata, trigger/view configuration, and enabled/published state remain unchanged unless exact `--replace-manifest --reason "..."` authority was provided for an existing resource.
|
|
128
132
|
|
|
129
133
|
An App Function may declare metadata-only top-level `secretRefs: [{ name, required }]` only with `function_v2` + `trusted_node_v2`; source resolves values with `await ctx.secrets.get(name)` and uses `ctx.utils.http` for controlled public HTTPS. Create/rotate values through hidden TTY or `openxiangda secret ... --value-stdin --change <change> --profile <name>`. Never put values in arguments, files, manifests, state, plans, logs, errors, or chat. Secret bindings require `backend_release_v2` and whole-app `atomic_staged_children_v2`; a missing capability is fail-closed and never uses the legacy source PATCH. For whole-app activation use exact-scope `resource publish <type> --only <code> --stage-only`, then pass the returned verified `stagedResource` to `release app-finalize`; an active Backend Release is never labeled staged.
|
|
130
134
|
|
|
@@ -61,7 +61,7 @@ Selectors apply before manifest parsing, source dependency analysis, and JS_CODE
|
|
|
61
61
|
|
|
62
62
|
Function/Automation source analysis also extracts statically declared Form filter and order fields. `resource plan` compares them with each bound Form's frozen online schema and reports `formFieldContracts`; publish fails before lease/write when a binding or field is missing. Fix the source or stage the Form schema in the same reviewed release instead of waiting for a production SQL-column error. Dynamic field names remain runtime-validated by the platform and return `FORM_FIELD_NOT_FOUND` as a configuration error.
|
|
63
63
|
|
|
64
|
-
Source-triggered Function/Automation targets use Backend Release v2 when the platform exposes that capability. One child may mix 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`. 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 requires exact `--only/--code` and `--replace-manifest --reason "<why>"`; SDD bypass does not imply replacement authority.
|
|
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
66
|
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
67
|
|
|
@@ -109,6 +109,8 @@ Once `environment init` registers a logical application, or `environment attach`
|
|
|
109
109
|
|
|
110
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.
|
|
111
111
|
|
|
112
|
+
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
|
+
|
|
112
114
|
`release publish` is the normal whole-app entrypoint for legacy unmanaged workspaces: it verifies SDD without mutating reviewed files, waits for the promotion lease, freezes one App capture, stages the exact Form/Backend/Runtime children, resumes from a private execution journal, finalizes once, and releases the lease. Managed applications use two-phase `release ship` and its deployment-scoped journal. Individual candidate/deploy/test/promote commands are recovery/diagnostic primitives.
|
|
113
115
|
|
|
114
116
|
Reviewed bundle commands may retain `<profile>` as a template. The explicit real `release publish --profile <name>` value is bound to actual child argv without rewriting tracked SDD. React SPA page codes remain logical coverage targets and activate through the single Runtime child; they do not require PageRelease. If local lease state disappears, `release end --change <id>` reconciles a self-owned remote lease from the private execution journal and never reports inactive while a remote lease is active.
|
|
@@ -86,6 +86,8 @@ openxiangda resource publish form-setting --only <formCode> --change <change> --
|
|
|
86
86
|
|
|
87
87
|
The Form resource bundle carries schema, settings, indexes, data-management, runtime-write, public-access configuration, and changed form permission groups under one CAS parent. It returns a canonical staged FormRelease and the CLI records it in the change-scoped staged-resources file. `workspace publish --form` is limited to an intentional first-time bootstrap or isolated repair outside a governed multi-resource release; it is not the normal release path.
|
|
88
88
|
|
|
89
|
+
An exact staged FormRelease remains usable across a fresh release baseline/lease for the same app/profile/change/deployment only after the CLI re-verifies its immutable, inactive, non-aborted server state, identity/hash, frozen schema/formType, finalized resources, parent/base revision, and current Form head. Workflow planning reads that frozen contract even when the live Form head is intentionally not activated and local `schemaSyncedAt` is absent. Never synthesize `schemaSyncedAt`, direct-publish the schema, or activate the Form early as a workaround; all conflicts fail closed.
|
|
90
|
+
|
|
89
91
|
For Phase 6 React SPA workspaces, `app-workspace.config.ts` should declare
|
|
90
92
|
`runtimeMode: "react-spa"`. In that mode, `workspace publish --form <code>` is
|
|
91
93
|
schema-only by default: it creates/binds the form and syncs schema, but skips
|
package/package.json
CHANGED
|
@@ -28,5 +28,5 @@ openxiangda resource typegen --profile <name>
|
|
|
28
28
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
29
29
|
- Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
|
|
30
30
|
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
31
|
-
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state.
|
|
31
|
+
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`.
|
|
32
32
|
- Formal promotion freezes the clean publish HEAD separately from the change base, preflights the complete set, and rejects `SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT`. After activation, merge/fast-forward and push the frozen SHA, verify `release integration-status`, then run normal `release end`; squash/rebase does not preserve the released source.
|
|
@@ -33,7 +33,9 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
33
33
|
- Roles that create roles, assign members, grant API permissions, maintain permission groups, or manage organization accounts must declare `apiPermissionCodes`, such as `app:role:manage`, `app:page-permission-group:manage`, `app:form-permission-group:manage`, and `app:organization:manage`.
|
|
34
34
|
- `src/resources/**` is the resource source of truth; use `validate -> plan -> publish`.
|
|
35
35
|
- Select logical resource codes with `--only`, or one code with `--code`; type-wide/app-wide release requires an approved dependency closure.
|
|
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.
|
|
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
|
+
- 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 --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.
|
|
37
39
|
- React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
|
|
38
40
|
- 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.
|
|
39
41
|
- Backend permissions, public-access grants, and App Function role/scope checks are authoritative.
|
|
@@ -28,5 +28,5 @@ openxiangda resource typegen --profile <name>
|
|
|
28
28
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
29
29
|
- Exact selectors apply before manifest/source analysis and JS_CODE build: touch only selected targets plus transitive/shared/ambient dependencies; omit selectors only for intentional full-workspace work.
|
|
30
30
|
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
31
|
-
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state.
|
|
31
|
+
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. A new source-free Automation with a complete `definitionJson.version="v3"` automatically uses manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`.
|
|
32
32
|
- Formal promotion freezes the clean publish HEAD separately from the change base, preflights the complete set, and rejects `SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT`. After activation, merge/fast-forward and push the frozen SHA, verify `release integration-status`, then run normal `release end`; squash/rebase does not preserve the released source.
|
|
@@ -29,7 +29,9 @@ This is an OpenXiangda React SPA workspace. Read [AGENTS.md](AGENTS.md) for full
|
|
|
29
29
|
- 角色能新增角色、分配成员、授接口权限、维护权限组或管理组织账号时,角色资源必须声明 `apiPermissionCodes`,例如 `app:role:manage`、`app:page-permission-group:manage`、`app:form-permission-group:manage`、`app:organization:manage`。
|
|
30
30
|
- `src/resources/**` 是工程化资源来源,正式多资源变更走 `validate -> plan -> publish`。
|
|
31
31
|
- 默认按逻辑资源 code 使用 `--only` 或单资源 `--code`;全类型/全应用发布必须由批准的依赖闭包明确覆盖。
|
|
32
|
-
- Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state
|
|
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
|
+
- 相同 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 --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
33
35
|
- Promotion 必须持有 `release begin/end` 租约;`release begin` 只接受与权威远端 tip 完全一致的 clean main/master。feature branch 或未 push 主线在任何写入前失败。激活后直接运行 `integration-status` 和 `release end`,不再补做发布后合并。
|
|
34
36
|
- 已有工作区通过 `environment attach` 接入环境组,旧资源映射只迁移到 appType 相同的预发 target,正式 target 必须为空。preproduction / production 分别拥有独立 appType、资源 ID、数据和副作用策略;禁止直接 `release publish`,禁止跨环境复制 ID,使用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
|
|
35
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 模式才阻断。
|
|
@@ -51,12 +51,14 @@ openxiangda studio
|
|
|
51
51
|
openxiangda commands --json
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
模板已停用无范围的 `pnpm deploy` 聚合入口。日常变更必须使用 `resource plan|publish <type> --only <codes>`(单资源可用 `--code <code>`)。Form bundle、Backend Release 和 Runtime 都先暂存;CLI 会按 `--change` 自动聚合 `.openxiangda/releases/<change>/staged-resources.json`,最后只由一次 Root App finalize 原子激活。Form Release 一旦 abort 绝不能作为幂等结果复用;重新执行相同精确表单发布时,CLI 会淘汰旧 staged 索引,平台会创建新的不可变 attempt,再由 Root App
|
|
54
|
+
模板已停用无范围的 `pnpm deploy` 聚合入口。日常变更必须使用 `resource plan|publish <type> --only <codes>`(单资源可用 `--code <code>`)。Form bundle、Backend Release 和 Runtime 都先暂存;CLI 会按 `--change` 自动聚合 `.openxiangda/releases/<change>/staged-resources.json`,最后只由一次 Root App finalize 原子激活。Form Release 一旦 abort 绝不能作为幂等结果复用;重新执行相同精确表单发布时,CLI 会淘汰旧 staged 索引,平台会创建新的不可变 attempt,再由 Root App 一次事务重试。相同 change 的既有 staged FormRelease 只有在 CLI 重新核验服务端不可变状态、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 和当前 Form Head 后,才可重挂接到新的 baseline/session;`schemaSyncedAt` 只是本地缓存元数据。禁止伪造它、提前激活 Form 或用顺序激活多张表单绕过失败。不要使用 `workspace publish --form`、单独 `runtime activate`、`pnpm publish:all`、`pnpm openxiangda:publish` 或 `lowcode-workspace publish-all`。
|
|
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
58
|
只有已审计代码早已进入权威主线、而精确非删除目标在线上来自多次历史发布且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`。该参数不会放松冻结 online heads、change/lease、删除/全量拒绝、服务端 CAS、staged children 或单次 App finalize 原子激活。
|
|
59
59
|
|
|
60
|
+
环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只会进入精确 Backend selector,正式确认必须复用与预发完全相同的参数,绝不扩散到表单、流程、Runtime、配置资源或全量范围。
|
|
61
|
+
|
|
60
62
|
受支持的配置资源(如 `publicAccessPolicies`)以精确 `resourceSelectors` 为发布边界;历史 `resources=true` 类别标记会被精确 selector 收窄。缺失 selector、未知类型、通配符 `*`、删除和真正全量资源仍必须 fail closed。
|
|
61
63
|
|
|
62
64
|
`resource plan` 与 publish dry-run 严格只允许 GET/HEAD。遇到 `READ_ONLY_AUTH_REQUIRED` 时,先执行 `openxiangda auth refresh --profile <name>` 或重新登录再重试;不得在 plan 内自动 POST 刷新 token。
|
|
@@ -80,7 +82,7 @@ App Function 第三方凭据只能在 Function manifest 顶层声明 `secretRefs
|
|
|
80
82
|
|
|
81
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`。
|
|
82
84
|
|
|
83
|
-
Function/Automation 走 Backend Release v2;同一个 child
|
|
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 会在任何平台写入前失败。成功激活后主线证据天然成立,不再补做发布后合并。
|
|
84
86
|
|
|
85
87
|
## 应用结构
|
|
86
88
|
|
|
@@ -31,7 +31,7 @@ Exact `--only/--code` selectors apply before manifest/source analysis and JS_COD
|
|
|
31
31
|
|
|
32
32
|
App Function manifests may contain only logical names in top-level `secretRefs`; use `function_v2` / `trusted_node_v2`, resolve values with `ctx.secrets.get(name)`, and manage values only with `openxiangda secret ... --value-stdin --change ... --profile ...`. Values never belong in Git, `.env`, manifests, source, builds, plans, logs, or errors.
|
|
33
33
|
|
|
34
|
-
When a Function/Automation enters scope only through source changes, publishing uses a server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state.
|
|
34
|
+
When a Function/Automation enters scope only through source changes, publishing uses a 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 manifest create; replacing an existing whole manifest requires `--replace-manifest --reason "..."`. Formal promotion freezes the clean publish HEAD separately from the change/remote baseline and preflights the whole set; `SOURCE_BASE_DIVERGED`, `RELEASE_SOURCE_BEHIND_MAIN`, and `RESOURCE_FIELD_CONFLICT` require reconciliation. After activation, merge/push the frozen SHA, verify `release integration-status`, then run normal `release end`.
|
|
35
35
|
|
|
36
36
|
Before editing `roles`, `permissions/page-groups`, or `permissions/form-groups` for account/role/data-scope/RBAC/query-param authorization work, run `openxiangda design gates --topic permissions --json`, choose the permission mode, and write the permission matrix.
|
|
37
37
|
|
|
@@ -29,7 +29,9 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
|
|
|
29
29
|
- 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.
|
|
30
30
|
- Very small copy/style/binding changes may use `openxiangda sdd quick <change> ...`; quick mode is limited to an exact low-risk scope and does not add a redundant proposal/approval loop when the user already requested that exact edit.
|
|
31
31
|
- SDD is streamlined by default: approval and exact structured scope are hard gates, while unfinished task/evidence/spec prose only warns. Use `strictDocumentation: true` only when prose must block.
|
|
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.
|
|
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
|
+
- 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
|
+
- 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.
|
|
33
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.
|
|
34
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.
|
|
35
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.
|
|
@@ -41,7 +41,7 @@ openxiangda resource publish <type> --code <code> --profile <name> # 单资源
|
|
|
41
41
|
openxiangda resource pull --profile <name> # 平台 → 本地
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Function/Automation 仅因源码变化进入 scope 时,默认通过服务端字段 PATCH 只更新 source snapshot,并保留线上 bindings、contracts、metadata、trigger/view 配置和 enabled/published state
|
|
44
|
+
Function/Automation 仅因源码变化进入 scope 时,默认通过服务端字段 PATCH 只更新 source snapshot,并保留线上 bindings、contracts、metadata、trigger/view 配置和 enabled/published state。无源码且 `definitionJson.version="v3"` 完整的新建 Automation 自动走 manifest create;确需让本地整包 manifest 覆盖已有线上定义时,必须追加 `--replace-manifest --reason "..."`。正式发布先 `release begin --change <change>` 分别冻结 clean publish HEAD 与 change/远端 baseline 并整体 preflight;`SOURCE_BASE_DIVERGED` / `RELEASE_SOURCE_BEHIND_MAIN` / `RESOURCE_FIELD_CONFLICT` 不能强行重试。上线后 merge/push 冻结 SHA,验证 `release integration-status`,再正常 `release end`。
|
|
45
45
|
|
|
46
46
|
`resource plan` 与 publish dry-run 严格只允许 GET/HEAD。遇到 `READ_ONLY_AUTH_REQUIRED` 时,先执行 `openxiangda auth refresh --profile <name>` 或重新登录再重试;不得在 plan 内自动 POST 刷新 token。
|
|
47
47
|
|
|
@@ -29,7 +29,9 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
|
|
|
29
29
|
- L0 只读/文档/测试无需 SDD;L1 窄小可逆改动记录精确范围且不重复确认;表单结构、业务函数、自动化/流程、权限、登录/公开访问、数据写入、runtime/config 等 L2/L3 走完整 SDD,live evidence/archive 放到发布后。
|
|
30
30
|
- 极小的文案、样式、绑定修正可使用 `openxiangda sdd quick <change> ...`,但必须限制在精确的低风险范围内;用户已明确要求该小改时不再重复 propose/approve。
|
|
31
31
|
- SDD 默认 streamlined:approval 与结构化精确范围是硬门禁,未完成的 task/evidence/spec 文案只告警;只有 `strictDocumentation: true` 才阻断。
|
|
32
|
-
- Function/Automation 源码触发默认走服务端 source-field PATCH,保留线上 bindings/contracts/metadata/trigger/view/enabled/published state
|
|
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
|
+
- 相同 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 --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
33
35
|
- 写入平台前只从与权威远端 tip 完全一致的 clean main/master 执行 `release begin`。feature branch 或未 push 主线在任何写入前失败;激活后直接运行 `integration-status` 和 `release end`,无需发布后再合并。
|
|
34
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,且默认不放开副作用。
|
|
35
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 模式才阻断。
|
|
@@ -55,10 +55,12 @@
|
|
|
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
|
|
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 "..."`。
|
|
59
|
+
- ✅ 相同 change 已有 staged FormRelease 时,不要直发 schema、伪造 `schemaSyncedAt` 或提前激活 Form。CLI 只在重新核验服务端不可变状态、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才将 child 重挂接到新的 baseline/session;冲突继续失败关闭。
|
|
59
60
|
- ✅ 未登记环境的旧工作区,正式 promotion 先聚合 mainline bundle 并 commit/push,再运行 `release publish --change <id> --profile <name>`。
|
|
60
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 模式才阻断。
|
|
61
62
|
- ✅ 只有已审计目标早已进入权威主线、线上却由多次历史 lineage 组成且无法对应单一 Git 基线时,第一次 `release ship` 才可增加 `--adopt-online-baseline --adoption-reason "..."`;仅允许精确非删除 selectors,冻结 Head、change/lease、服务端 CAS、staged children 与单次 App finalize 仍是硬门禁。
|
|
63
|
+
- ✅ 环境托管发布需要完整替换 Function/Automation manifest 时,`release ship` 可成对增加 `--replace-manifest --reason "..."`;reason 至少 8 字符,只透传精确 Backend selector,正式确认必须复用同一对参数,不扩散到 Form/Workflow/Runtime/配置或全量范围。
|
|
62
64
|
- ✅ 受支持的配置资源(如 `publicAccessPolicies`)以精确 `resourceSelectors` 为边界;历史 `resources=true` 类别标记会被精确 selector 收窄。缺失 selector、未知类型、通配符 `*`、删除和真正全量资源仍 fail closed。
|
|
63
65
|
- ✅ 本地开发者可运行 `openxiangda studio` 查看两套环境、差异、候选、部署和测试证据;该页面只监听回环地址且只暴露注册动作,生产操作仍需显式确认。
|
|
64
66
|
|