openxiangda 1.0.206 → 1.0.208
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 -1
- package/lib/application-environments.js +48 -0
- package/lib/cli.js +419 -43
- package/openxiangda-skills/SKILL.md +3 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +2 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +2 -0
- package/package.json +1 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +2 -1
- package/templates/openxiangda-react-spa/AGENTS.md +2 -2
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +2 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +2 -1
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -1
package/README.md
CHANGED
|
@@ -202,12 +202,14 @@ 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
|
|
|
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
|
|
@@ -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,
|
|
@@ -597,6 +644,7 @@ module.exports = {
|
|
|
597
644
|
normalizeEnvironmentKind,
|
|
598
645
|
normalizeManagedChangeSourceBase,
|
|
599
646
|
normalizeManagedReleaseSourceRevision,
|
|
647
|
+
normalizeReleaseSourceRevisionForBaseline,
|
|
600
648
|
normalizeSideEffectPolicyInput,
|
|
601
649
|
normalizeTargetName,
|
|
602
650
|
hasStateResourceMappings,
|
package/lib/cli.js
CHANGED
|
@@ -136,6 +136,7 @@ const {
|
|
|
136
136
|
normalizeEnvironmentKind,
|
|
137
137
|
normalizeManagedChangeSourceBase,
|
|
138
138
|
normalizeManagedReleaseSourceRevision,
|
|
139
|
+
normalizeReleaseSourceRevisionForBaseline,
|
|
139
140
|
readCandidate,
|
|
140
141
|
readJsonInput,
|
|
141
142
|
releaseExecutionPath,
|
|
@@ -3834,7 +3835,10 @@ async function release(args) {
|
|
|
3834
3835
|
);
|
|
3835
3836
|
}
|
|
3836
3837
|
const pendingBaseline = getStoredChangeBaseline(target);
|
|
3837
|
-
const newReleaseLifecycle = !
|
|
3838
|
+
const newReleaseLifecycle = !existing && !pendingBaseline;
|
|
3839
|
+
const reusableFormCandidates = newReleaseLifecycle
|
|
3840
|
+
? reusableStagedFormReleaseCandidates(target, changeId)
|
|
3841
|
+
: [];
|
|
3838
3842
|
const clientSessionId =
|
|
3839
3843
|
existing?.clientSessionId ||
|
|
3840
3844
|
(pendingBaseline?.changeId === changeId &&
|
|
@@ -3866,6 +3870,7 @@ async function release(args) {
|
|
|
3866
3870
|
let frozenCapture = null;
|
|
3867
3871
|
let releaseSessionFile = null;
|
|
3868
3872
|
let stagedResourcesFile = null;
|
|
3873
|
+
let reusedStagedFormReleases = [];
|
|
3869
3874
|
try {
|
|
3870
3875
|
if (flags['freeze-app-capture']) {
|
|
3871
3876
|
frozenCapture = await fetchAppReleaseCapture(config, target, flags);
|
|
@@ -3877,9 +3882,16 @@ async function release(args) {
|
|
|
3877
3882
|
frozenCapture
|
|
3878
3883
|
);
|
|
3879
3884
|
}
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3885
|
+
if (newReleaseLifecycle) {
|
|
3886
|
+
const initialized = await initializeNewReleaseStagedResources(
|
|
3887
|
+
config,
|
|
3888
|
+
target,
|
|
3889
|
+
changeId,
|
|
3890
|
+
reusableFormCandidates
|
|
3891
|
+
);
|
|
3892
|
+
stagedResourcesFile = initialized.file;
|
|
3893
|
+
reusedStagedFormReleases = initialized.reused;
|
|
3894
|
+
}
|
|
3883
3895
|
} catch (error) {
|
|
3884
3896
|
if (newReleaseLifecycle) {
|
|
3885
3897
|
try {
|
|
@@ -3914,6 +3926,9 @@ async function release(args) {
|
|
|
3914
3926
|
}
|
|
3915
3927
|
: {}),
|
|
3916
3928
|
...(stagedResourcesFile ? { stagedResourcesFile } : {}),
|
|
3929
|
+
...(reusedStagedFormReleases.length > 0
|
|
3930
|
+
? { reusedStagedFormReleases }
|
|
3931
|
+
: {}),
|
|
3917
3932
|
};
|
|
3918
3933
|
if (flags.json) return writeJson(result);
|
|
3919
3934
|
print(
|
|
@@ -4893,14 +4908,22 @@ function writeStagedResourcesFiles(
|
|
|
4893
4908
|
return path.relative(process.cwd(), file).replace(/\\/g, '/');
|
|
4894
4909
|
}
|
|
4895
4910
|
|
|
4896
|
-
function initializeStagedAppReleaseResources(
|
|
4911
|
+
function initializeStagedAppReleaseResources(
|
|
4912
|
+
target,
|
|
4913
|
+
changeId,
|
|
4914
|
+
resources = [],
|
|
4915
|
+
metadata = {}
|
|
4916
|
+
) {
|
|
4897
4917
|
const context = currentStagedResourcesContext(target, changeId);
|
|
4898
4918
|
return writeStagedResourcesFiles(
|
|
4899
4919
|
changeId,
|
|
4900
|
-
|
|
4920
|
+
Array.isArray(resources) && resources.length === 0
|
|
4921
|
+
? []
|
|
4922
|
+
: normalizeStagedAppReleaseResources(resources),
|
|
4901
4923
|
{
|
|
4902
4924
|
...context,
|
|
4903
4925
|
initializedAt: new Date().toISOString(),
|
|
4926
|
+
...metadata,
|
|
4904
4927
|
},
|
|
4905
4928
|
target.deploymentId
|
|
4906
4929
|
);
|
|
@@ -5083,6 +5106,278 @@ function stagedFormCode(target, resource) {
|
|
|
5083
5106
|
return formUuid;
|
|
5084
5107
|
}
|
|
5085
5108
|
|
|
5109
|
+
function reusableStagedFormReleaseCandidates(target, changeId) {
|
|
5110
|
+
const normalizedChangeId = String(changeId || '').trim();
|
|
5111
|
+
if (!normalizedChangeId) return [];
|
|
5112
|
+
const file = stagedResourcesFileForChange(
|
|
5113
|
+
normalizedChangeId,
|
|
5114
|
+
target.deploymentId
|
|
5115
|
+
);
|
|
5116
|
+
if (!fs.existsSync(file)) return [];
|
|
5117
|
+
const contextFile = stagedResourcesContextFileForChange(
|
|
5118
|
+
normalizedChangeId,
|
|
5119
|
+
target.deploymentId
|
|
5120
|
+
);
|
|
5121
|
+
if (!fs.existsSync(contextFile)) {
|
|
5122
|
+
fail(
|
|
5123
|
+
`FORM_RELEASE_REUSE_CONTEXT_REQUIRED: change ${normalizedChangeId} 的 staged FormRelease 缺少 context,拒绝重挂接`
|
|
5124
|
+
);
|
|
5125
|
+
}
|
|
5126
|
+
const context = JSON.parse(fs.readFileSync(contextFile, 'utf8'));
|
|
5127
|
+
const expected = {
|
|
5128
|
+
contractVersion: 'staged_resources_context_v1',
|
|
5129
|
+
appType: target.appType,
|
|
5130
|
+
profile: target.profileName,
|
|
5131
|
+
changeId: normalizedChangeId,
|
|
5132
|
+
deploymentId: target.deploymentId || null,
|
|
5133
|
+
};
|
|
5134
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
5135
|
+
if ((context?.[field] ?? null) !== (value ?? null)) {
|
|
5136
|
+
fail(
|
|
5137
|
+
`FORM_RELEASE_REUSE_CONTEXT_MISMATCH: ${field} 不属于当前 app/profile/change/deployment,拒绝重挂接 staged FormRelease`
|
|
5138
|
+
);
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
5142
|
+
const resources = Array.isArray(parsed) && parsed.length === 0
|
|
5143
|
+
? []
|
|
5144
|
+
: normalizeStagedAppReleaseResources(parsed);
|
|
5145
|
+
let scopedForms = null;
|
|
5146
|
+
try {
|
|
5147
|
+
scopedForms = new Set(
|
|
5148
|
+
getSddChangeScope({
|
|
5149
|
+
cwd: process.cwd(),
|
|
5150
|
+
configText: readWorkspaceConfigText(),
|
|
5151
|
+
changeId: normalizedChangeId,
|
|
5152
|
+
}).targets.forms || []
|
|
5153
|
+
);
|
|
5154
|
+
} catch (error) {
|
|
5155
|
+
// A direct release may use --change without a local SDD record. The
|
|
5156
|
+
// exact local context still supplies the app/profile/change scope, while
|
|
5157
|
+
// every candidate is independently verified against immutable server data.
|
|
5158
|
+
if (!/SDD 未初始化|change 不存在|ENOENT/i.test(String(error?.message || ''))) {
|
|
5159
|
+
throw error;
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
return resources
|
|
5163
|
+
.filter(resource => resource.kind === 'FormRelease')
|
|
5164
|
+
.map(resource => {
|
|
5165
|
+
const formCode = stagedFormCode(target, resource);
|
|
5166
|
+
if (
|
|
5167
|
+
scopedForms &&
|
|
5168
|
+
(!formCode || !scopedForms.has(formCode))
|
|
5169
|
+
) {
|
|
5170
|
+
fail(
|
|
5171
|
+
`FORM_RELEASE_REUSE_SCOPE_MISMATCH: ${formCode || resource.identity?.formUuid || '(unknown)'} 不在 change ${normalizedChangeId} 的精确 Form 范围内`
|
|
5172
|
+
);
|
|
5173
|
+
}
|
|
5174
|
+
return { resource, formCode };
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
function nullableReleaseId(value) {
|
|
5179
|
+
const normalized = String(value || '').trim();
|
|
5180
|
+
return normalized || null;
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
async function verifyReusableStagedFormRelease(
|
|
5184
|
+
config,
|
|
5185
|
+
target,
|
|
5186
|
+
candidate
|
|
5187
|
+
) {
|
|
5188
|
+
const resource = candidate.resource;
|
|
5189
|
+
const formCode = String(candidate.formCode || '').trim();
|
|
5190
|
+
const releaseId = String(resource.identity?.releaseId || '').trim();
|
|
5191
|
+
const formUuid = String(resource.identity?.formUuid || '').trim();
|
|
5192
|
+
const boundFormUuid = String(
|
|
5193
|
+
target.bound.resources?.forms?.[formCode]?.formUuid ||
|
|
5194
|
+
target.bound.resources?.formSettings?.[formCode]?.formUuid ||
|
|
5195
|
+
''
|
|
5196
|
+
).trim();
|
|
5197
|
+
if (!formCode || !boundFormUuid || boundFormUuid !== formUuid) {
|
|
5198
|
+
fail(
|
|
5199
|
+
`FORM_RELEASE_REUSE_BINDING_MISMATCH: ${formCode || formUuid || '(unknown)'} 的本地 binding 与 staged FormRelease identity 不一致`
|
|
5200
|
+
);
|
|
5201
|
+
}
|
|
5202
|
+
const basePath = `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/forms/${encodeURIComponent(formUuid)}`;
|
|
5203
|
+
const [detail, head] = await Promise.all([
|
|
5204
|
+
requestWithAuth(
|
|
5205
|
+
config,
|
|
5206
|
+
target.profileName,
|
|
5207
|
+
`${basePath}/releases/${encodeURIComponent(releaseId)}`
|
|
5208
|
+
),
|
|
5209
|
+
requestWithAuth(
|
|
5210
|
+
config,
|
|
5211
|
+
target.profileName,
|
|
5212
|
+
`${basePath}/releases/head`
|
|
5213
|
+
),
|
|
5214
|
+
]);
|
|
5215
|
+
if (
|
|
5216
|
+
detail?.immutable !== true ||
|
|
5217
|
+
detail?.active === true ||
|
|
5218
|
+
detail?.aborted === true ||
|
|
5219
|
+
(Array.isArray(detail?.journal) ? detail.journal : []).some(
|
|
5220
|
+
entry => entry?.action === 'abort'
|
|
5221
|
+
)
|
|
5222
|
+
) {
|
|
5223
|
+
fail(
|
|
5224
|
+
`FORM_RELEASE_REUSE_STATE_INVALID: ${formCode} 的 FormRelease ${releaseId} 必须 immutable=true、active=false、aborted=false`
|
|
5225
|
+
);
|
|
5226
|
+
}
|
|
5227
|
+
if (
|
|
5228
|
+
String(detail?.id || '').trim() !== releaseId ||
|
|
5229
|
+
String(detail?.formUuid || '').trim() !== formUuid
|
|
5230
|
+
) {
|
|
5231
|
+
fail(
|
|
5232
|
+
`FORM_RELEASE_REUSE_IDENTITY_MISMATCH: ${formCode} 的 releaseId/formUuid 与服务端冻结证据不一致`
|
|
5233
|
+
);
|
|
5234
|
+
}
|
|
5235
|
+
const contentHash = String(detail?.contentHash || '')
|
|
5236
|
+
.trim()
|
|
5237
|
+
.toLowerCase();
|
|
5238
|
+
if (
|
|
5239
|
+
!/^[a-f0-9]{64}$/.test(contentHash) ||
|
|
5240
|
+
contentHash !== String(resource.hash || '').trim().toLowerCase()
|
|
5241
|
+
) {
|
|
5242
|
+
fail(
|
|
5243
|
+
`FORM_RELEASE_REUSE_HASH_MISMATCH: ${formCode} 的 staged hash 与 immutable FormRelease 不一致`
|
|
5244
|
+
);
|
|
5245
|
+
}
|
|
5246
|
+
const parentReleaseId = nullableReleaseId(detail?.parentReleaseId);
|
|
5247
|
+
const baseRevision = Number(detail?.baseRevision);
|
|
5248
|
+
if (
|
|
5249
|
+
nullableReleaseId(resource.revision?.parentReleaseId) !==
|
|
5250
|
+
parentReleaseId ||
|
|
5251
|
+
Number(resource.revision?.baseRevision) !== baseRevision
|
|
5252
|
+
) {
|
|
5253
|
+
fail(
|
|
5254
|
+
`FORM_RELEASE_REUSE_REVISION_MISMATCH: ${formCode} 的 staged parent/baseRevision 与 immutable FormRelease 不一致`
|
|
5255
|
+
);
|
|
5256
|
+
}
|
|
5257
|
+
const activeHead = head?.activeFormReleaseHead || {};
|
|
5258
|
+
const currentRevision = Number(head?.revision ?? activeHead.revision);
|
|
5259
|
+
if (
|
|
5260
|
+
nullableReleaseId(activeHead.releaseId) !== parentReleaseId ||
|
|
5261
|
+
!Number.isSafeInteger(currentRevision) ||
|
|
5262
|
+
currentRevision !== baseRevision
|
|
5263
|
+
) {
|
|
5264
|
+
fail(
|
|
5265
|
+
`FORM_RELEASE_REUSE_PARENT_CONFLICT: ${formCode} 的 active head/revision 已变化,拒绝重挂接 staged FormRelease`
|
|
5266
|
+
);
|
|
5267
|
+
}
|
|
5268
|
+
if (
|
|
5269
|
+
!Array.isArray(detail?.resources) ||
|
|
5270
|
+
detail.resources.length === 0 ||
|
|
5271
|
+
detail.resources.some(item => !item?.finalizedAt)
|
|
5272
|
+
) {
|
|
5273
|
+
fail(
|
|
5274
|
+
`FORM_RELEASE_REUSE_RESOURCES_INVALID: ${formCode} 的 FormRelease 资源尚未全部 finalized`
|
|
5275
|
+
);
|
|
5276
|
+
}
|
|
5277
|
+
const frozenForm = detail?.snapshotJson?.form;
|
|
5278
|
+
if (!frozenForm || frozenForm.schema === undefined || !frozenForm.formType) {
|
|
5279
|
+
fail(
|
|
5280
|
+
`FORM_RELEASE_REUSE_SCHEMA_MISSING: ${formCode} 的 FormRelease 缺少冻结 schema/formType`
|
|
5281
|
+
);
|
|
5282
|
+
}
|
|
5283
|
+
let frozenSchema = frozenForm.schema;
|
|
5284
|
+
if (typeof frozenSchema === 'string') {
|
|
5285
|
+
try {
|
|
5286
|
+
frozenSchema = JSON.parse(frozenSchema);
|
|
5287
|
+
} catch {
|
|
5288
|
+
fail(
|
|
5289
|
+
`FORM_RELEASE_REUSE_SCHEMA_INVALID: ${formCode} 的 FormRelease 冻结 schema 不是合法 JSON`
|
|
5290
|
+
);
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
if (!frozenSchema || typeof frozenSchema !== 'object') {
|
|
5294
|
+
fail(
|
|
5295
|
+
`FORM_RELEASE_REUSE_SCHEMA_INVALID: ${formCode} 的 FormRelease 冻结 schema 无效`
|
|
5296
|
+
);
|
|
5297
|
+
}
|
|
5298
|
+
return {
|
|
5299
|
+
resource: {
|
|
5300
|
+
kind: 'FormRelease',
|
|
5301
|
+
identity: { releaseId, formUuid },
|
|
5302
|
+
action: resource.action || 'update',
|
|
5303
|
+
hash: contentHash,
|
|
5304
|
+
revision: { parentReleaseId, baseRevision },
|
|
5305
|
+
metadata: {
|
|
5306
|
+
...(resource.metadata || {}),
|
|
5307
|
+
formCode,
|
|
5308
|
+
releaseStatus: 'staged',
|
|
5309
|
+
},
|
|
5310
|
+
},
|
|
5311
|
+
formCode,
|
|
5312
|
+
formUuid,
|
|
5313
|
+
releaseId,
|
|
5314
|
+
contentHash,
|
|
5315
|
+
baseRevision,
|
|
5316
|
+
parentReleaseId,
|
|
5317
|
+
formSnapshot: {
|
|
5318
|
+
...clonePlainJson(frozenForm),
|
|
5319
|
+
schema: clonePlainJson(frozenSchema),
|
|
5320
|
+
},
|
|
5321
|
+
formType: frozenForm.formType,
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
|
|
5325
|
+
async function resolveReusableStagedFormReleaseOverlays(
|
|
5326
|
+
config,
|
|
5327
|
+
target,
|
|
5328
|
+
changeId,
|
|
5329
|
+
options = {}
|
|
5330
|
+
) {
|
|
5331
|
+
const requested = new Set(
|
|
5332
|
+
(options.formCodes || [])
|
|
5333
|
+
.map(value => String(value || '').trim())
|
|
5334
|
+
.filter(Boolean)
|
|
5335
|
+
);
|
|
5336
|
+
const candidates = reusableStagedFormReleaseCandidates(target, changeId)
|
|
5337
|
+
.filter(candidate => requested.size === 0 || requested.has(candidate.formCode));
|
|
5338
|
+
const verified = [];
|
|
5339
|
+
for (const candidate of candidates) {
|
|
5340
|
+
verified.push(
|
|
5341
|
+
await verifyReusableStagedFormRelease(config, target, candidate)
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
return new Map(verified.map(item => [item.formCode, item]));
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
async function initializeNewReleaseStagedResources(
|
|
5348
|
+
config,
|
|
5349
|
+
target,
|
|
5350
|
+
changeId,
|
|
5351
|
+
candidates
|
|
5352
|
+
) {
|
|
5353
|
+
const verified = [];
|
|
5354
|
+
for (const candidate of candidates || []) {
|
|
5355
|
+
verified.push(
|
|
5356
|
+
await verifyReusableStagedFormRelease(config, target, candidate)
|
|
5357
|
+
);
|
|
5358
|
+
}
|
|
5359
|
+
const reused = verified.map(item => ({
|
|
5360
|
+
formCode: item.formCode,
|
|
5361
|
+
formUuid: item.formUuid,
|
|
5362
|
+
releaseId: item.releaseId,
|
|
5363
|
+
contentHash: item.contentHash,
|
|
5364
|
+
baseRevision: item.baseRevision,
|
|
5365
|
+
parentReleaseId: item.parentReleaseId,
|
|
5366
|
+
}));
|
|
5367
|
+
const file = initializeStagedAppReleaseResources(
|
|
5368
|
+
target,
|
|
5369
|
+
changeId,
|
|
5370
|
+
verified.map(item => item.resource),
|
|
5371
|
+
reused.length > 0
|
|
5372
|
+
? {
|
|
5373
|
+
reusedStagedFormReleases: reused,
|
|
5374
|
+
reusedAt: new Date().toISOString(),
|
|
5375
|
+
}
|
|
5376
|
+
: {}
|
|
5377
|
+
);
|
|
5378
|
+
return { file, reused };
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5086
5381
|
function assertStagedAppReleaseScope(target, flags, resources) {
|
|
5087
5382
|
const changeId = readStringFlag(flags, 'change');
|
|
5088
5383
|
if (!changeId) {
|
|
@@ -6083,6 +6378,15 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
6083
6378
|
const explicitLeaseId = readStringFlag(flags, 'publish-lease-id');
|
|
6084
6379
|
const changeId = readStringFlag(flags, 'change');
|
|
6085
6380
|
const storedLease = getStoredPublishLease(target);
|
|
6381
|
+
const baselineAtStart = changeId
|
|
6382
|
+
? getStoredChangeBaseline(target)
|
|
6383
|
+
: null;
|
|
6384
|
+
const newReleaseLifecycle = Boolean(
|
|
6385
|
+
changeId && !getUsableStoredPublishLease(target) && !baselineAtStart
|
|
6386
|
+
);
|
|
6387
|
+
const reusableFormCandidates = newReleaseLifecycle
|
|
6388
|
+
? reusableStagedFormReleaseCandidates(target, changeId)
|
|
6389
|
+
: [];
|
|
6086
6390
|
let acquiredByCommand = false;
|
|
6087
6391
|
assertOrClaimWorktreeOwner({
|
|
6088
6392
|
cwd: process.cwd(),
|
|
@@ -6152,7 +6456,41 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
|
|
|
6152
6456
|
clearRejectedPendingBaseline(target, baseline, error);
|
|
6153
6457
|
throw error;
|
|
6154
6458
|
}
|
|
6155
|
-
finalizeReleaseBaseline(target, baseline);
|
|
6459
|
+
const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
|
|
6460
|
+
if (newReleaseLifecycle) {
|
|
6461
|
+
try {
|
|
6462
|
+
await initializeNewReleaseStagedResources(
|
|
6463
|
+
config,
|
|
6464
|
+
target,
|
|
6465
|
+
changeId,
|
|
6466
|
+
reusableFormCandidates
|
|
6467
|
+
);
|
|
6468
|
+
} catch (error) {
|
|
6469
|
+
try {
|
|
6470
|
+
await requestWithAuth(
|
|
6471
|
+
config,
|
|
6472
|
+
target.profileName,
|
|
6473
|
+
publishLeaseApiPath(
|
|
6474
|
+
target,
|
|
6475
|
+
`/${encodeURIComponent(lease.leaseId)}/release`
|
|
6476
|
+
),
|
|
6477
|
+
{
|
|
6478
|
+
method: 'POST',
|
|
6479
|
+
body: { completion: 'local-prepare-failed' },
|
|
6480
|
+
}
|
|
6481
|
+
);
|
|
6482
|
+
} catch {
|
|
6483
|
+
// Keep the staged child verification failure as the primary error.
|
|
6484
|
+
}
|
|
6485
|
+
clearPublishLease(target, lease.leaseId);
|
|
6486
|
+
clearChangeBaseline(
|
|
6487
|
+
target,
|
|
6488
|
+
finalizedBaseline?.baselineId || finalizedBaseline?.id
|
|
6489
|
+
);
|
|
6490
|
+
releaseWorktreeOwner({ cwd: process.cwd() });
|
|
6491
|
+
throw error;
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6156
6494
|
}
|
|
6157
6495
|
if (!lease) return null;
|
|
6158
6496
|
if (changeId && lease.changeId !== changeId) {
|
|
@@ -12635,9 +12973,34 @@ async function resource(args) {
|
|
|
12635
12973
|
|
|
12636
12974
|
const validation = validateWorkspaceResources(manifest);
|
|
12637
12975
|
let target = null;
|
|
12976
|
+
let stagedFormBindingOverlays = new Map();
|
|
12638
12977
|
if (!isManifestEmpty(manifest)) {
|
|
12639
12978
|
target = getWorkspaceTarget(config, profileName, flags);
|
|
12640
|
-
|
|
12979
|
+
const workflowFormCodes = unique(
|
|
12980
|
+
(manifest.workflows || [])
|
|
12981
|
+
.map(item => item.formCode || item.form)
|
|
12982
|
+
.filter(Boolean)
|
|
12983
|
+
);
|
|
12984
|
+
const stagedChangeId =
|
|
12985
|
+
readStringFlag(flags, 'change') ||
|
|
12986
|
+
getStoredChangeBaseline(target, { access: 'reconciliation-read' })
|
|
12987
|
+
?.changeId ||
|
|
12988
|
+
'';
|
|
12989
|
+
if (workflowFormCodes.length > 0 && stagedChangeId) {
|
|
12990
|
+
stagedFormBindingOverlays =
|
|
12991
|
+
await resolveReusableStagedFormReleaseOverlays(
|
|
12992
|
+
config,
|
|
12993
|
+
target,
|
|
12994
|
+
stagedChangeId,
|
|
12995
|
+
{ formCodes: workflowFormCodes }
|
|
12996
|
+
);
|
|
12997
|
+
}
|
|
12998
|
+
validateWorkspaceResourceBindings(
|
|
12999
|
+
manifest,
|
|
13000
|
+
target.bound,
|
|
13001
|
+
validation,
|
|
13002
|
+
stagedFormBindingOverlays
|
|
13003
|
+
);
|
|
12641
13004
|
}
|
|
12642
13005
|
await validateCompiledWorkflowResources(manifest, validation);
|
|
12643
13006
|
if (subcommand === 'validate') {
|
|
@@ -16123,7 +16486,12 @@ function validateWorkspaceResources(manifest) {
|
|
|
16123
16486
|
};
|
|
16124
16487
|
}
|
|
16125
16488
|
|
|
16126
|
-
function validateWorkspaceResourceBindings(
|
|
16489
|
+
function validateWorkspaceResourceBindings(
|
|
16490
|
+
manifest,
|
|
16491
|
+
bound,
|
|
16492
|
+
validation,
|
|
16493
|
+
stagedFormBindingOverlays = new Map()
|
|
16494
|
+
) {
|
|
16127
16495
|
for (const item of manifest.workflows || []) {
|
|
16128
16496
|
if (!item.formCode && !item.form) continue;
|
|
16129
16497
|
const formCode = item.formCode || item.form;
|
|
@@ -16136,14 +16504,25 @@ function validateWorkspaceResourceBindings(manifest, bound, validation) {
|
|
|
16136
16504
|
continue;
|
|
16137
16505
|
}
|
|
16138
16506
|
const localSchemaPath = path.join(process.cwd(), 'src', 'forms', formCode, 'schema.ts');
|
|
16139
|
-
|
|
16507
|
+
const stagedOverlay = stagedFormBindingOverlays.get(formCode);
|
|
16508
|
+
if (
|
|
16509
|
+
fs.existsSync(localSchemaPath) &&
|
|
16510
|
+
!formBinding.schemaSyncedAt &&
|
|
16511
|
+
!stagedOverlay
|
|
16512
|
+
) {
|
|
16140
16513
|
validation.errors.push(
|
|
16141
16514
|
`${resourceLabel('workflow', item)}: formCode ${formCode} 已绑定但本地 schema 尚未同步。请先运行 openxiangda workspace publish --profile <name> --form ${formCode}`
|
|
16142
16515
|
);
|
|
16143
16516
|
}
|
|
16144
|
-
if (
|
|
16517
|
+
if (stagedOverlay) {
|
|
16518
|
+
validation.warnings.push(
|
|
16519
|
+
`${resourceLabel('workflow', item)}: formCode ${formCode} 使用已验证 staged FormRelease ${stagedOverlay.releaseId} 的冻结 schema/formType`
|
|
16520
|
+
);
|
|
16521
|
+
}
|
|
16522
|
+
const effectiveFormType = stagedOverlay?.formType || formBinding.formType;
|
|
16523
|
+
if (effectiveFormType && effectiveFormType !== 'process') {
|
|
16145
16524
|
validation.errors.push(
|
|
16146
|
-
`${resourceLabel('workflow', item)}: formCode ${formCode} 当前 formType=${
|
|
16525
|
+
`${resourceLabel('workflow', item)}: formCode ${formCode} 当前 formType=${effectiveFormType},流程表单需要 process`
|
|
16147
16526
|
);
|
|
16148
16527
|
}
|
|
16149
16528
|
}
|
|
@@ -17397,8 +17776,12 @@ async function buildResourcePlan(
|
|
|
17397
17776
|
await prepareManifestJsCodeBundlesForPlan(manifest);
|
|
17398
17777
|
const stagedLocalFormSnapshots =
|
|
17399
17778
|
await resolveStagedLocalFormContractSnapshots(
|
|
17779
|
+
config,
|
|
17400
17780
|
target,
|
|
17401
|
-
options.flags || {}
|
|
17781
|
+
options.flags || {},
|
|
17782
|
+
(manifest.workflows || [])
|
|
17783
|
+
.map(item => item.formCode || item.form)
|
|
17784
|
+
.filter(Boolean)
|
|
17402
17785
|
);
|
|
17403
17786
|
const [existing, formFieldContracts] = await Promise.all([
|
|
17404
17787
|
fetchExistingResourceMaps(config, target, manifest),
|
|
@@ -17828,8 +18211,9 @@ async function ensureOwnedImmutableReleasePublishContext(
|
|
|
17828
18211
|
`${releaseKind} 只接受与本地冻结基线匹配的 stored lease;外部 opaque lease 不能用于原子发布`
|
|
17829
18212
|
);
|
|
17830
18213
|
}
|
|
17831
|
-
const sourceRevision =
|
|
18214
|
+
const sourceRevision = normalizeReleaseSourceRevisionForBaseline(
|
|
17832
18215
|
target,
|
|
18216
|
+
baseline.sourceBase || {},
|
|
17833
18217
|
baseline.releaseSourceRevision || {},
|
|
17834
18218
|
{ errorCode: 'RELEASE_SOURCE_REPOSITORY_MISMATCH' }
|
|
17835
18219
|
);
|
|
@@ -26855,32 +27239,38 @@ async function runWorkspaceChildCommand(command, args, options = {}) {
|
|
|
26855
27239
|
}
|
|
26856
27240
|
|
|
26857
27241
|
async function resolveStagedLocalFormContractSnapshots(
|
|
27242
|
+
config,
|
|
26858
27243
|
target,
|
|
26859
|
-
flags = {}
|
|
27244
|
+
flags = {},
|
|
27245
|
+
implicitFormCodes = []
|
|
26860
27246
|
) {
|
|
26861
|
-
const
|
|
27247
|
+
const explicitFormCodes = unique(
|
|
26862
27248
|
String(flags['staged-form-contracts'] || '')
|
|
26863
27249
|
.split(',')
|
|
26864
27250
|
.map(value => value.trim())
|
|
26865
27251
|
.filter(Boolean)
|
|
26866
27252
|
).sort();
|
|
27253
|
+
const formCodes = unique([
|
|
27254
|
+
...explicitFormCodes,
|
|
27255
|
+
...(implicitFormCodes || [])
|
|
27256
|
+
.map(value => String(value || '').trim())
|
|
27257
|
+
.filter(Boolean),
|
|
27258
|
+
]).sort();
|
|
26867
27259
|
if (formCodes.length === 0) return new Map();
|
|
26868
27260
|
const changeId = readStringFlag(flags, 'change');
|
|
26869
27261
|
if (!changeId) {
|
|
27262
|
+
if (explicitFormCodes.length === 0) return new Map();
|
|
26870
27263
|
fail(
|
|
26871
27264
|
'FORM_FIELD_STAGED_CONTRACT_CHANGE_REQUIRED: --staged-form-contracts 必须与 --change 一起使用'
|
|
26872
27265
|
);
|
|
26873
27266
|
}
|
|
26874
|
-
const
|
|
26875
|
-
|
|
26876
|
-
|
|
26877
|
-
|
|
26878
|
-
|
|
26879
|
-
|
|
26880
|
-
|
|
26881
|
-
JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
26882
|
-
)
|
|
26883
|
-
: [];
|
|
27267
|
+
const stagedOverlays =
|
|
27268
|
+
await resolveReusableStagedFormReleaseOverlays(
|
|
27269
|
+
config,
|
|
27270
|
+
target,
|
|
27271
|
+
changeId,
|
|
27272
|
+
{ formCodes }
|
|
27273
|
+
);
|
|
26884
27274
|
const snapshots = new Map();
|
|
26885
27275
|
for (const formCode of formCodes) {
|
|
26886
27276
|
const formUuid = resolveManifestFormUuid(
|
|
@@ -26888,29 +27278,15 @@ async function resolveStagedLocalFormContractSnapshots(
|
|
|
26888
27278
|
{ formCode },
|
|
26889
27279
|
{ fallbackToCode: false }
|
|
26890
27280
|
);
|
|
26891
|
-
const staged =
|
|
26892
|
-
|
|
26893
|
-
resource.kind === 'FormRelease' &&
|
|
26894
|
-
String(resource.identity?.formUuid || '').trim() === formUuid
|
|
26895
|
-
);
|
|
26896
|
-
if (!staged) {
|
|
27281
|
+
const staged = stagedOverlays.get(formCode);
|
|
27282
|
+
if (!staged || staged.formUuid !== formUuid) {
|
|
26897
27283
|
fail(
|
|
26898
27284
|
`FORM_FIELD_STAGED_CONTRACT_REQUIRED: ${formCode} 尚未在 change ${changeId} 的当前 deployment 中获得 verified FormRelease`
|
|
26899
27285
|
);
|
|
26900
27286
|
}
|
|
26901
|
-
const localSchema = await loadLocalFormSchema(
|
|
26902
|
-
process.cwd(),
|
|
26903
|
-
formCode
|
|
26904
|
-
);
|
|
26905
|
-
if (!localSchema) {
|
|
26906
|
-
fail(
|
|
26907
|
-
`FORM_FIELD_STAGED_CONTRACT_SCHEMA_MISSING: ${formCode} 缺少本地 schema`
|
|
26908
|
-
);
|
|
26909
|
-
}
|
|
26910
|
-
const schema = JSON.parse(localSchema.schema);
|
|
26911
27287
|
snapshots.set(
|
|
26912
27288
|
formUuid,
|
|
26913
|
-
collectFormSnapshotFieldIds({ form:
|
|
27289
|
+
collectFormSnapshotFieldIds({ form: staged.formSnapshot })
|
|
26914
27290
|
);
|
|
26915
27291
|
}
|
|
26916
27292
|
return snapshots;
|
|
@@ -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
|
|
|
@@ -126,6 +126,8 @@ Because promotion begins from the already-pushed authoritative mainline, `releas
|
|
|
126
126
|
|
|
127
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.
|
|
128
128
|
|
|
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
|
+
|
|
129
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.
|
|
130
132
|
|
|
131
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.
|
|
@@ -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.
|
|
@@ -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
|
@@ -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.
|
|
@@ -34,6 +34,7 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
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
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.
|
|
37
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.
|
|
38
39
|
- React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
|
|
39
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.
|
|
@@ -30,8 +30,9 @@ This is an OpenXiangda React SPA workspace. Read [AGENTS.md](AGENTS.md) for full
|
|
|
30
30
|
- `src/resources/**` 是工程化资源来源,正式多资源变更走 `validate -> plan -> publish`。
|
|
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
|
+
- 相同 change 的 staged FormRelease 只有经服务端重新核验 immutable/inactive/non-aborted、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才可重挂接新 baseline/session。`schemaSyncedAt` 不是发布证据;禁止伪造、直发 schema 或提前激活 Form 绕过 Workflow 校验。
|
|
33
34
|
- 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
34
|
-
- Promotion 必须持有 `release begin/end` 租约;`release begin` 只接受与权威远端 tip 完全一致的 clean main/master。feature branch 或未 push
|
|
35
|
+
- Promotion 必须持有 `release begin/end` 租约;`release begin` 只接受与权威远端 tip 完全一致的 clean main/master。feature branch 或未 push 主线在任何写入前失败。clone primary 只有在冻结仓库 ID 已存在于 `repoAliases` 时才会 canonicalize;无交集在 Release prepare 前失败关闭。激活后直接运行 `integration-status` 和 `release end`,不再补做发布后合并。
|
|
35
36
|
- 已有工作区通过 `environment attach` 接入环境组,旧资源映射只迁移到 appType 相同的预发 target,正式 target 必须为空。preproduction / production 分别拥有独立 appType、资源 ID、数据和副作用策略;禁止直接 `release publish`,禁止跨环境复制 ID,使用 `openxiangda studio` 查看状态。只有用户明确授权的投产前重分类可执行 `environment swap --reason "..." --confirm-production`;它不移动应用数据或 Release Head,且默认不放开副作用。
|
|
36
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 模式才阻断。
|
|
37
38
|
- React SPA 路由由 `src/app/router.tsx` 管理,前端包通过 `openxiangda runtime deploy` 发布。
|
|
@@ -51,7 +51,7 @@ 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
|
|
|
@@ -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
|
|
|
@@ -30,8 +30,9 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. See [AGEN
|
|
|
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
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.
|
|
33
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.
|
|
34
|
-
- 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.
|
|
35
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.
|
|
36
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.
|
|
37
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`.
|
|
@@ -30,8 +30,9 @@ This is a `sy-lowcode-app-workspace` managed by the `openxiangda` CLI. Read [AGE
|
|
|
30
30
|
- 极小的文案、样式、绑定修正可使用 `openxiangda sdd quick <change> ...`,但必须限制在精确的低风险范围内;用户已明确要求该小改时不再重复 propose/approve。
|
|
31
31
|
- SDD 默认 streamlined:approval 与结构化精确范围是硬门禁,未完成的 task/evidence/spec 文案只告警;只有 `strictDocumentation: true` 才阻断。
|
|
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
|
+
- 相同 change 的 staged FormRelease 只有经服务端重新核验 immutable/inactive/non-aborted、identity/hash、冻结 schema/formType、finalized 资源、parent/base revision 与当前 Form Head 后,才可重挂接新 baseline/session。`schemaSyncedAt` 不是发布证据;禁止伪造、直发 schema 或提前激活 Form 绕过 Workflow 校验。
|
|
33
34
|
- 环境托管 `release ship --replace-manifest --reason "..."` 必须成对、reason 至少 8 字符且仅限精确 Backend selector;正式确认复用与预发完全相同的参数,不透传 Form/Workflow/Runtime/配置或全量步骤。
|
|
34
|
-
- 写入平台前只从与权威远端 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`,无需发布后再合并。
|
|
35
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,且默认不放开副作用。
|
|
36
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 模式才阻断。
|
|
37
38
|
- 单文件改动默认按 change 和逻辑目标发布:先 `workspace plan --profile <name> --change <change> --changed`,再 `workspace publish --profile <name> --change <change> --only pages/a,forms/b --dry-run`。
|
|
@@ -55,7 +55,8 @@
|
|
|
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
|
+
- ✅ 相同 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 仍是硬门禁。
|