openxiangda 1.0.166 → 1.0.168

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 CHANGED
@@ -56,7 +56,7 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
56
56
 
57
57
  React SPA workspaces publish their frontend with `openxiangda runtime deploy`. Every finalized Runtime release is built from a clean, committed Git `HEAD`; the CLI freezes `sourceRevision`, the current active release, and its source revision before any build/upload. Deploy fails with `RUNTIME_SOURCE_BASE_DIVERGED` when its `HEAD` does not descend from the online Runtime source, including with `--no-activate`, so an old isolated worktree cannot stage and later activate a silent rollback. `.openxiangda/`, `openspec/`, `dist/`, and other pure generated/governance/state paths do not make the source dirty, but `--no-build` cannot bypass the lineage gate. An intentional rollback requires `--allow-runtime-rollback --reason "<at least 8 characters>"`, which is persisted for audit and never bypasses dirty/non-Git checks.
58
58
 
59
- Concurrent development may still upload immutable previews with `--no-activate`, but the preview must also freeze a real clean Git source revision before it can become a release. Final promotion uses `release begin/end` so one `(tenant, app)` has one writer while other worktrees keep developing and testing. `release begin --change <change>` binds the per-task `clientSessionId` (Codex uses `CODEX_THREAD_ID`), keeps the earlier change base separate, and freezes the actual clean publish `HEAD` plus the selected remote resource-field heads. That publish commit must already contain the current authoritative remote default branch, and it cannot change during the release. Before the first live write, the CLI preflights the complete selected set. A stale resource worktree fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field that moved after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve the divergence, rebuild/re-plan, and start a new release instead of retrying a stale payload. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result.
59
+ Concurrent development may still upload immutable previews with `--no-activate`, but the preview must also freeze a real clean Git source revision before it can become a release. Final promotion uses `release begin/end` so one `(tenant, app)` has one writer while other worktrees keep developing and testing. `release begin --change <change>` binds the per-task `clientSessionId` (Codex uses `CODEX_THREAD_ID`), keeps the earlier change base separate, and freezes the actual clean publish `HEAD` plus the selected remote resource-field heads. That publish commit must already contain the current authoritative remote default branch, and it cannot change during the release. Before the first live write, the CLI preflights the complete selected set. A stale resource worktree fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field that moved after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve the divergence, rebuild/re-plan, and start a new release instead of retrying a stale payload. The only exception is a historical catch-up where approved code was already merged to main and the live app demonstrably consists of several older release lineages: exact, non-delete publishes may use `--adopt-online-baseline --adoption-reason "<at least 8 characters>"` together with the same `--change` (or its already-held release lease). This adopts the server-frozen online Function/Automation heads instead of inventing one Git base; Function/Automation still run the server remote-head preflight, and every resource remains inside the app lease, staged-release/CAS path supported by its publisher. It rejects `--all`, `--force`, `--prune`, and every delete plan. The default upload mode is `auto`: it tries staged multipart first, then falls back to built-in OSS direct upload if the staged file API returns HTTP 403. Use `--upload-mode oss-direct` to force built-in OSS direct upload, or `--upload-mode legacy-json` only as a compatibility fallback for older platforms. Upload progress, timeout, and `traceId` are written to stderr; `--json` stdout remains the final JSON result.
60
60
 
61
61
  After a live promotion, merge or fast-forward the exact frozen publish commit into the authoritative default branch and push it before closing the task. `openxiangda release integration-status --profile <name>` checks the live remote tip; normal `release end` keeps the reconciliation evidence and returns `RELEASE_MAIN_MERGE_PENDING` until that exact SHA is present. Squash/rebase does not preserve the published commit and therefore does not pass. The authoritative target is always the live default `main`/`master` of `origin` (or the repository's sole remote); CLI flags, environment variables, local-only branches, and lease expiry cannot bypass it. OpenXiangda's own `npm publish` and the platform image release script apply the stronger pre-release form of the same rule: root and pinned submodule commits must already be in their remote default branches.
62
62
 
package/lib/cli.js CHANGED
@@ -8665,6 +8665,7 @@ async function resource(args) {
8665
8665
  ' - Function/Automation 可用 --stage-only 只 prepare + verify Backend Release;随后把返回的 stagedResource 交给 release app-finalize 原子激活。',
8666
8666
  ' - publish --dry-run 等价于发布前计划,不会写平台资源。',
8667
8667
  ' - 在线 publish 必须用 --only/--code 精确选择;真正全量发布必须显式 --all --reason,SDD bypass 不能绕过此范围门。',
8668
+ ' - 历史代码已先合入主线、线上又来自多次分批发布时,可显式使用 --adopt-online-baseline --adoption-reason "...";仅允许绑定同一 change/lease 的精确非删除范围,仍保留 lease、冻结 head 检查与写入 CAS。',
8668
8669
  ' - 源码依赖触发的 Function/Automation 更新默认只替换 source snapshot,保留线上 bindings/schema/metadata;显式替换 manifest 必须加 --replace-manifest --reason。',
8669
8670
  ' - --only/--code 会在读取/分析 manifest 与构建源码前按逻辑资源 code 早期收窄;也支持 function:foo 这类带类型 selector。',
8670
8671
  ' - type 可选: workflow, notification, route, menu, role, permission, data-view, function, connector 等。',
@@ -8931,6 +8932,45 @@ function assertExplicitManifestReplacement(flags) {
8931
8932
  }
8932
8933
  }
8933
8934
 
8935
+ function resolveOnlineBaselineAdoption(
8936
+ flags = {},
8937
+ planActions = [],
8938
+ publishLease = null
8939
+ ) {
8940
+ if (!flags['adopt-online-baseline']) return null;
8941
+ const reason = String(
8942
+ flags['adoption-reason'] || flags.reason || ''
8943
+ ).trim();
8944
+ if (reason.length < 8) {
8945
+ fail(
8946
+ '--adopt-online-baseline 必须提供至少 8 个字符的 --adoption-reason,说明为何线上资源无法对应单一 Git 基线'
8947
+ );
8948
+ }
8949
+ const changeId = String(flags.change || publishLease?.changeId || '').trim();
8950
+ if (!changeId) {
8951
+ fail(
8952
+ '--adopt-online-baseline 必须与 --change <id> 或已持有的同 change 发布租约一起使用'
8953
+ );
8954
+ }
8955
+ if (flags.all || flags.force || flags.prune) {
8956
+ fail('--adopt-online-baseline 禁止与 --all、--force 或 --prune 一起使用');
8957
+ }
8958
+ if (!flags.only && !flags.code) {
8959
+ fail('--adopt-online-baseline 只允许 --only/--code 精确资源范围');
8960
+ }
8961
+ const deletes = (planActions || []).filter(
8962
+ action => action?.action === 'delete'
8963
+ );
8964
+ if (deletes.length > 0) {
8965
+ fail('--adopt-online-baseline 禁止任何 delete 计划');
8966
+ }
8967
+ return {
8968
+ mode: 'frozen_online_heads_v1',
8969
+ changeId,
8970
+ reason,
8971
+ };
8972
+ }
8973
+
8934
8974
  function buildResourceManifestSddTargets(manifest, planActions = []) {
8935
8975
  const codes = key => unique((manifest[key] || []).map(getResourceItemCode).filter(Boolean)).sort();
8936
8976
  const deletedCodes = kind =>
@@ -14483,6 +14523,7 @@ function runGenericResourceGitBasePreflight({
14483
14523
  baseline,
14484
14524
  publishLease,
14485
14525
  targets,
14526
+ onlineBaselineAdoption = null,
14486
14527
  }) {
14487
14528
  if (!targets.length) return null;
14488
14529
  const publishLeaseId = String(
@@ -14508,6 +14549,26 @@ function runGenericResourceGitBasePreflight({
14508
14549
  const selectors = unique(
14509
14550
  targets.map(item => `${item.resourceKey}:${item.code}`)
14510
14551
  ).sort();
14552
+ if (onlineBaselineAdoption) {
14553
+ return {
14554
+ ok: true,
14555
+ baselineId: baseline.baselineId,
14556
+ publishLeaseId,
14557
+ headDigest,
14558
+ baseCommit,
14559
+ checkedTargets: targets.map(({ kind, code, action }) => ({
14560
+ kind,
14561
+ code,
14562
+ action,
14563
+ })),
14564
+ selectors,
14565
+ planRuns: 0,
14566
+ cached: false,
14567
+ readOnly: true,
14568
+ gitBasePlanSkipped: true,
14569
+ onlineBaselineAdoption,
14570
+ };
14571
+ }
14511
14572
  const typeFilters = unique(manifest.resourceTypeFilters || []).sort();
14512
14573
  const cacheKey = canonicalJsonSha256({
14513
14574
  publishLeaseId,
@@ -14697,6 +14758,11 @@ async function runResourceChangeBaselinePreflight(
14697
14758
  flags = {},
14698
14759
  publishLease = null
14699
14760
  ) {
14761
+ const onlineBaselineAdoption = resolveOnlineBaselineAdoption(
14762
+ flags,
14763
+ planActions,
14764
+ publishLease
14765
+ );
14700
14766
  const genericTargets = buildGenericResourceGitBaseTargets(planActions, {
14701
14767
  force: Boolean(flags.force),
14702
14768
  });
@@ -14720,19 +14786,22 @@ async function runResourceChangeBaselinePreflight(
14720
14786
  };
14721
14787
  } else {
14722
14788
  const artifactCache = { ...(baseline.localBaseArtifactHashes || {}) };
14723
- const missingArtifactTargets = targets.filter(targetItem => {
14724
- const frozen = baseline.resourceHeads?.[targetItem.kind]?.[targetItem.code];
14725
- const cached = artifactCache[
14726
- resourceBaselineTargetKey(targetItem.kind, targetItem.code)
14727
- ];
14728
- const hasStableLineage =
14729
- frozen?.fields?.sourceLineageHash &&
14730
- frozen.fields.sourceLineageHash !== canonicalJsonSha256(null);
14731
- return (
14732
- frozen?.fields?.sourceArtifactHash &&
14733
- (!cached || (hasStableLineage && !cached.sourceHash))
14734
- );
14735
- });
14789
+ const missingArtifactTargets = onlineBaselineAdoption
14790
+ ? []
14791
+ : targets.filter(targetItem => {
14792
+ const frozen =
14793
+ baseline.resourceHeads?.[targetItem.kind]?.[targetItem.code];
14794
+ const cached = artifactCache[
14795
+ resourceBaselineTargetKey(targetItem.kind, targetItem.code)
14796
+ ];
14797
+ const hasStableLineage =
14798
+ frozen?.fields?.sourceLineageHash &&
14799
+ frozen.fields.sourceLineageHash !== canonicalJsonSha256(null);
14800
+ return (
14801
+ frozen?.fields?.sourceArtifactHash &&
14802
+ (!cached || (hasStableLineage && !cached.sourceHash))
14803
+ );
14804
+ });
14736
14805
  if (missingArtifactTargets.length > 0) {
14737
14806
  const built = buildGitBaseSourceArtifacts({
14738
14807
  cwd: process.cwd(),
@@ -14758,6 +14827,13 @@ async function runResourceChangeBaselinePreflight(
14758
14827
  };
14759
14828
  }
14760
14829
  const fields = { ...(frozen.fields || {}) };
14830
+ if (onlineBaselineAdoption) {
14831
+ return {
14832
+ kind: targetItem.kind,
14833
+ code: targetItem.code,
14834
+ fields,
14835
+ };
14836
+ }
14761
14837
  const artifact = artifactCache[
14762
14838
  resourceBaselineTargetKey(targetItem.kind, targetItem.code)
14763
14839
  ];
@@ -14813,6 +14889,12 @@ async function runResourceChangeBaselinePreflight(
14813
14889
  },
14814
14890
  },
14815
14891
  );
14892
+ if (onlineBaselineAdoption) {
14893
+ serverPreflight = {
14894
+ ...serverPreflight,
14895
+ onlineBaselineAdoption,
14896
+ };
14897
+ }
14816
14898
  }
14817
14899
 
14818
14900
  const genericResourcePreflight = runGenericResourceGitBasePreflight({
@@ -14821,6 +14903,7 @@ async function runResourceChangeBaselinePreflight(
14821
14903
  baseline,
14822
14904
  publishLease,
14823
14905
  targets: genericTargets,
14906
+ onlineBaselineAdoption,
14824
14907
  });
14825
14908
  return genericResourcePreflight
14826
14909
  ? { ...serverPreflight, genericResourcePreflight }
@@ -19,6 +19,8 @@ const TEMPLATE_IGNORE_NAMES = new Set([
19
19
  const TEMPLATE_IGNORE_PATHS = new Set([
20
20
  '.openxiangda/build-cache.json',
21
21
  '.openxiangda/build-cache.json.lock',
22
+ '.openxiangda/build-cache.cli-v4.json',
23
+ '.openxiangda/build-cache.cli-v4.json.lock',
22
24
  '.openxiangda/worktree-owner.json',
23
25
  '.openxiangda/worktree-owner.json.lock',
24
26
  ]);
@@ -93,7 +93,7 @@ openxiangda release end --profile <name>
93
93
 
94
94
  `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.
95
95
 
96
- `release begin --change` separately freezes the change base and the actual publish source: a clean committed `HEAD` on a named branch that already contains the current authoritative remote default branch. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A worktree based on obsolete source fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field changed after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve/merge, rebuild, and re-plan instead of forcing the old release.
96
+ `release begin --change` separately freezes the change base and the actual publish source: a clean committed `HEAD` on a named branch that already contains the current authoritative remote default branch. Before any live write, the CLI preflights the complete target set and rejects source changes during the release. A worktree based on obsolete source fails with `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`; a selected remote field changed after the baseline fails with `RESOURCE_FIELD_CONFLICT`. Resolve/merge, rebuild, and re-plan instead of forcing the old release. For a one-time historical catch-up only—approved code already merged to main while live resources come from several older release lineages—an exact non-delete publish may add `--adopt-online-baseline --adoption-reason "..."` under the same change/lease. It adopts the server-frozen heads and keeps lease, remote preflight, staged release, and CAS protection; it rejects `--all`, `--force`, `--prune`, and deletes.
97
97
 
98
98
  After promotion, immediately merge or fast-forward the exact frozen publish commit into the authoritative remote default branch and push it. `release integration-status` reports the result; normal `release end` refuses with `RELEASE_MAIN_MERGE_PENDING` until the remote mainline contains that exact SHA, so squash/rebase merges do not count. The authoritative target comes only from the live remote default `main`/`master`; local-only refs, overrides, and lease expiry cannot clear this obligation. Archive the SDD change from the updated mainline.
99
99
 
@@ -130,7 +130,7 @@ openxiangda workspace bind --profile dev --app-type APP_XXXX
130
130
 
131
131
  - Wrong/missing profile: stop before writes and resolve `env`/`auth status`.
132
132
  - Dirty shared checkout: isolate the task; do not broaden its change coverage.
133
- - `SOURCE_BASE_DIVERGED`: the worktree no longer descends from the frozen Git base; merge/rebase onto the reviewed head, rebuild, and start a new release.
133
+ - `SOURCE_BASE_DIVERGED`: the worktree no longer descends from the frozen Git base; merge onto the reviewed head, rebuild, and start a new release. Only when approved code was historically merged before release and the online app has multiple verified release lineages may an exact non-delete catch-up use `--adopt-online-baseline --adoption-reason "..."` under the same change/lease; never use it for a stale branch, delete, prune, force, or full-app wildcard.
134
134
  - `RESOURCE_FIELD_CONFLICT`: a selected remote field changed after the frozen baseline; fetch the conflicting fields, reconcile the intended change, and re-plan the whole set before writing anything.
135
135
  - `RUNTIME_HEAD_MOVED` or resource revision conflict: fetch the new baseline, merge, rebuild, and re-plan.
136
136
  - `RUNTIME_SOURCE_BASE_DIVERGED`: the Runtime build comes from an old/different Git lineage; merge the online release source into a clean committed `HEAD`, rebuild, and retry. Use audited rollback only when the rollback itself is the approved intent.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda",
3
- "version": "1.0.166",
3
+ "version": "1.0.168",
4
4
  "description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
5
5
  "private": false,
6
6
  "bin": {
@@ -72,7 +72,7 @@ App Function 第三方凭据只能在 Function manifest 顶层声明 `secretRefs
72
72
 
73
73
  `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`。
74
74
 
75
- Function/Automation 走 Backend Release v2;同一个 child 可以混合 create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。正式多资源发布必须使用精确 selector 和 `--stage-only`,不得回退为逐资源直写;整包 manifest 替换还必须显式加 `--replace-manifest --reason "..."`。`release begin --change` 会分别冻结 change base 与实际 clean publish HEAD,并要求后者包含当前远端主分支;旧 worktree 返回 `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`,期间远端字段变化返回 `RESOURCE_FIELD_CONFLICT`。上线后先 merge/push 冻结 SHA;正常 `release end` 会以 `RELEASE_MAIN_MERGE_PENDING` 阻断未回合提交。权威目标只取远端实时默认 `main`/`master`,本地分支、覆盖参数和租约过期都不能清除证据。
75
+ Function/Automation 走 Backend Release v2;同一个 child 可以混合 create、source-only update 与显式 manifest replacement,并对整个集合做 CAS。正式多资源发布必须使用精确 selector 和 `--stage-only`,不得回退为逐资源直写;整包 manifest 替换还必须显式加 `--replace-manifest --reason "..."`。`release begin --change` 会分别冻结 change base 与实际 clean publish HEAD,并要求后者包含当前远端主分支;旧 worktree 返回 `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`,期间远端字段变化返回 `RESOURCE_FIELD_CONFLICT`。仅历史代码已先合入主线且线上经确认来自多个旧发布血缘时,精确非删除补发可在同一 change/lease 下加 `--adopt-online-baseline --adoption-reason "..."`;该模式禁止 `--all/--force/--prune/delete`,并保留冻结 head、lease 与 CAS。上线后先 merge/push 冻结 SHA;正常 `release end` 会以 `RELEASE_MAIN_MERGE_PENDING` 阻断未回合提交。权威目标只取远端实时默认 `main`/`master`,本地分支、覆盖参数和租约过期都不能清除证据。
76
76
 
77
77
  ## 应用结构
78
78
 
@@ -53,7 +53,7 @@
53
53
  - ✅ 正式多资源开发优先写 `src/resources/**` 后执行 `openxiangda resource validate|plan|publish <type> --only <codes>`;单资源可用 `--code <code>`。直接 CLI 写平台资源时先 `--dry-run`,需要避免漂移就加 `--write-manifest`。
54
54
  - ✅ `resource plan` 与 publish dry-run 严格只允许 GET/HEAD;遇到 `READ_ONLY_AUTH_REQUIRED` 时先执行 `openxiangda auth refresh --profile <name>` 或重新登录,不得在 plan 内自动 POST 刷新 token。
55
55
  - ✅ Function/Automation 使用 Backend Release v2;正式多资源发布用精确 `--only/--code` 加 `--stage-only` 暂存,同一 child 可混合 create、source-only update 和显式 manifest replacement,再由 Root App finalize 原子激活。整包 manifest 替换必须另加 `--replace-manifest --reason "..."`。
56
- - ✅ 正式 promotion 先 `openxiangda release begin --change <change> --profile <name>`,冻结实际 clean publish HEAD + change/远端字段 baseline,并在任何 live write 前整体 preflight。旧 worktree 会收到 `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`,期间远端字段变化会收到 `RESOURCE_FIELD_CONFLICT`。所有 publish/activate 继续传同一 `--change`;上线后 merge/push 冻结 SHA,运行 `release integration-status`,再正常 `release end`。未回合会收到 `RELEASE_MAIN_MERGE_PENDING`;权威目标只取远端实时默认 `main`/`master`,本地分支、覆盖参数和租约过期都不能清除证据。
56
+ - ✅ 正式 promotion 先 `openxiangda release begin --change <change> --profile <name>`,冻结实际 clean publish HEAD + change/远端字段 baseline,并在任何 live write 前整体 preflight。旧 worktree 会收到 `SOURCE_BASE_DIVERGED`/`RELEASE_SOURCE_BEHIND_MAIN`,期间远端字段变化会收到 `RESOURCE_FIELD_CONFLICT`。只有历史代码已先合入主线、且线上确认来自多个旧发布血缘时,精确非删除补发才可在同一 change/lease 下使用 `--adopt-online-baseline --adoption-reason "..."`;禁止 `--all/--force/--prune/delete`。所有 publish/activate 继续传同一 `--change`;上线后 merge/push 冻结 SHA,运行 `release integration-status`,再正常 `release end`。未回合会收到 `RELEASE_MAIN_MERGE_PENDING`;权威目标只取远端实时默认 `main`/`master`,本地分支、覆盖参数和租约过期都不能清除证据。
57
57
 
58
58
  ## 严禁
59
59