openxiangda 1.0.172 → 1.0.173

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
@@ -41,11 +41,8 @@ openxiangda resource validate function --only customer_get,customer_save --profi
41
41
  openxiangda resource plan function --only customer_get,customer_save --profile dev
42
42
  # merge/push approved task commits to main, then create and push one release bundle
43
43
  openxiangda sdd bundle mainline-release --changes add-customer-page,fix-customer-api
44
- openxiangda sdd verify mainline-release --changed --stage prepublish --profile dev
45
- openxiangda release begin --change mainline-release --profile dev
46
- # execute the canonical staged commands emitted by the release plan
47
- openxiangda release integration-status --profile dev
48
- openxiangda release end --profile dev
44
+ # verify, wait for the lease, stage exact children, atomically finalize, and release
45
+ openxiangda release publish --change mainline-release --profile dev
49
46
  openxiangda resource explain public-access --json
50
47
  openxiangda inspect app --profile dev --json
51
48
  openxiangda app snapshot APP_XXXX --profile dev --json
@@ -57,7 +54,7 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
57
54
 
58
55
  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.
59
56
 
60
- Parallel tasks develop and test in isolated worktrees, but feature worktrees do not publish. After approved commits are merged and pushed, `sdd bundle <release-change> --changes ...` unions their exact structured scope. Commit/push the bundle, then run one promotion from a clean local main/master whose commit exactly equals the authoritative remote tip. `release begin` rejects feature branches and unpushed/diverged mainline before any platform write. Runtime build readiness is also checked before lease acquisition; the CLI executes the declared build through `npm run build`, which works with pnpm worktree symlinked dependencies without reinstalling or changing the lock file. The protected upload plan still precedes the actual build because its provider-specific `assetBaseUrl` is compiled into the bundle.
57
+ Parallel tasks develop and test in isolated worktrees, but feature worktrees do not publish. After approved commits are merged and pushed, `sdd bundle <release-change> --changes ...` unions their exact structured scope. Commit/push the bundle, then run `release publish --change <release-change> --profile <name>` from a clean local main/master whose commit exactly equals the authoritative remote tip. It verifies without rewriting reviewed SDD files, waits for the app lease, freezes one authoritative App capture, executes exact Form/Backend/Runtime staged steps, atomically finalizes the Root App release, and records a resumable local execution journal. Runtime checks use the narrow head endpoint; immutable Git-base artifact hashes are reused across plans.
61
58
 
62
59
  Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --profile <name>` should pass immediately after activation. Run it and `release end`; there is no post-release merge step.
63
60
 
@@ -426,7 +423,7 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
426
423
 
427
424
  App Function 访问第三方凭据时使用 `app_function_secrets_v1`:manifest 顶层只声明 `secretRefs: [{ "name": "dingtalk_org_app_key", "required": true }]`,同时使用 `definitionJson.version="function_v2"`、`runtimeContractVersion="trusted_node_v2"`;源码通过 `await ctx.secrets.get(name)` 解析,并通过 `ctx.utils.http` 访问受控公网 HTTPS(该桥接不会携带平台 Runtime token)。值只能经 `openxiangda secret create|rotate --value-stdin --change <id> --profile <name>` 或隐藏 TTY 输入,禁止进入 Git、manifest、源码、构建产物、plan、日志或异常。带 `secretRefs` 的 Function 必须走 `backend_release_v2`;需要整应用原子发布时先执行 `resource publish function --only <code> --stage-only`,再把返回的真实 `stagedResource` 交给 `release app-finalize --staged-resources-json ...` 完成 `atomic_staged_children_v2`。默认直接激活的 Backend Release 只返回 `activeResource`,不会伪装成 staged;旧平台 capability 不完整时 CLI 会失败关闭,绝不忽略绑定。
428
425
 
429
- 平台部门和账号管理走 app-scoped organization 能力。调用者必须在目标应用拥有 `app:organization:manage`;平台管理员天然可用,普通应用角色需要显式授权。Runtime service principal 不会直接放行,`ctx.organization` 会按真实操作人 / audit actor 校验权限。新接口只提供创建、更新、查询和密码专用操作,不提供删除;`account-update` 不能携带 `password`,重置他人密码必须走 `account-reset-password`,当前用户改密用 SDK / `ctx.organization.accounts.changeMyPassword({ oldPassword, newPassword })`。
426
+ 平台部门和账号管理走 app-scoped organization 能力。只读查询要求目标应用的 `app:organization:read` 或 `app:organization:manage`,创建、更新和密码操作要求 `app:organization:manage`;平台管理员天然可用,普通应用角色需要显式授权。Runtime service principal 不会直接放行,`ctx.organization` 会按真实操作人 / audit actor 校验权限。新接口不提供删除;`account-update` 不能携带 `password`,重置他人密码必须走 `account-reset-password`,当前用户改密用 SDK / `ctx.organization.accounts.changeMyPassword({ oldPassword, newPassword })`。
430
427
 
431
428
  CLI 写操作必须加 `--force`:
432
429
 
@@ -1,4 +1,8 @@
1
1
  const { spawnSync } = require('child_process');
2
+ const {
3
+ legacyRemoteIdentity,
4
+ remoteRepositoryHashes,
5
+ } = require('./git-remote-identity');
2
6
  const crypto = require('crypto');
3
7
  const fs = require('fs');
4
8
  const os = require('os');
@@ -6,6 +10,7 @@ const path = require('path');
6
10
 
7
11
  const { saveProjectState } = require('./config');
8
12
  const { runWorkspaceJsCodeBuildBatch } = require('./js-code-build');
13
+ const { version: OPENXIANGDA_VERSION } = require('../package.json');
9
14
 
10
15
  const SOURCE_KIND_BY_RESOURCE_KIND = Object.freeze({
11
16
  Function: 'functions',
@@ -232,14 +237,18 @@ function readGitSourceBase(cwd = process.cwd(), baseRef = 'HEAD') {
232
237
  : path.resolve(repository.root, commonGitDirValue)
233
238
  )
234
239
  : repository.root;
240
+ const repositoryIds = remote
241
+ ? remoteRepositoryHashes(remote)
242
+ : [];
235
243
  const identitySeed = remote
236
- ? `remote:${normalizeRemoteIdentity(remote)}`
244
+ ? `remote:${legacyRemoteIdentity(remote)}`
237
245
  : `local:${commonGitDir}`;
238
246
  const repositoryId = `sha256:${sha256Buffer(Buffer.from(identitySeed, 'utf8'))}`;
239
247
 
240
248
  return {
241
249
  repo: repositoryId,
242
250
  repositoryId,
251
+ repoAliases: Array.from(new Set([repositoryId, ...repositoryIds])),
243
252
  baseCommit,
244
253
  treeHash,
245
254
  };
@@ -253,7 +262,20 @@ function buildGitBaseSourceArtifacts({
253
262
  packageManager = 'pnpm',
254
263
  } = {}) {
255
264
  const normalizedTargets = normalizeArtifactTargets(targets);
256
- return withGitBaseWorkspace({ cwd, baseCommit, env }, context => {
265
+ const cacheFile =
266
+ env?.FAKE_BATCH_FAIL || env?.OPENXIANGDA_DISABLE_ARTIFACT_CACHE === '1'
267
+ ? null
268
+ : resolveGitArtifactCacheFile({
269
+ cwd,
270
+ baseCommit,
271
+ targets: normalizedTargets,
272
+ packageManager,
273
+ });
274
+ const cached = cacheFile
275
+ ? readGitArtifactCache(cacheFile, baseCommit, normalizedTargets)
276
+ : null;
277
+ if (cached) return { ...cached, cacheHit: true };
278
+ const built = withGitBaseWorkspace({ cwd, baseCommit, env }, context => {
257
279
  const {
258
280
  buildRoot,
259
281
  buildEnv,
@@ -349,6 +371,99 @@ function buildGitBaseSourceArtifacts({
349
371
  localBaseArtifactHashes,
350
372
  };
351
373
  });
374
+ if (cacheFile) writeGitArtifactCache(cacheFile, built);
375
+ return { ...built, cacheHit: false };
376
+ }
377
+
378
+ function resolveGitArtifactCacheFile({
379
+ cwd,
380
+ baseCommit,
381
+ targets,
382
+ packageManager,
383
+ }) {
384
+ const commit = String(baseCommit || '').trim().toLowerCase();
385
+ if (!/^[0-9a-f]{40,64}$/.test(commit)) return null;
386
+ const repository = resolveGitRepository(cwd);
387
+ const commonDirValue =
388
+ tryRunGit(repository.root, [
389
+ 'rev-parse',
390
+ '--path-format=absolute',
391
+ '--git-common-dir',
392
+ ]).trim() ||
393
+ tryRunGit(repository.root, ['rev-parse', '--git-common-dir']).trim();
394
+ if (!commonDirValue) return null;
395
+ const commonDir = path.resolve(repository.root, commonDirValue);
396
+ const key = canonicalJsonSha256({
397
+ schemaVersion: 'git_base_artifacts_v2',
398
+ openxiangdaVersion: OPENXIANGDA_VERSION,
399
+ baseCommit: commit,
400
+ packageManager,
401
+ targets,
402
+ });
403
+ return path.join(
404
+ commonDir,
405
+ 'openxiangda-cache',
406
+ 'git-base-artifacts-v2',
407
+ `${key}.json`
408
+ );
409
+ }
410
+
411
+ function readGitArtifactCache(file, baseCommit, targets) {
412
+ if (!fs.existsSync(file)) return null;
413
+ let value;
414
+ try {
415
+ value = JSON.parse(fs.readFileSync(file, 'utf8'));
416
+ } catch {
417
+ return null;
418
+ }
419
+ if (
420
+ value?.schemaVersion !== 'git_base_artifacts_v2' ||
421
+ value?.openxiangdaVersion !== OPENXIANGDA_VERSION ||
422
+ value?.result?.baseCommit !== String(baseCommit).toLowerCase()
423
+ ) {
424
+ return null;
425
+ }
426
+ const expectedKeys = targets
427
+ .map(target => `${target.kind}:${target.code}`)
428
+ .sort();
429
+ const artifactKeys = Object.keys(value.result.artifacts || {}).sort();
430
+ if (
431
+ expectedKeys.length !== artifactKeys.length ||
432
+ expectedKeys.some((key, index) => key !== artifactKeys[index])
433
+ ) {
434
+ return null;
435
+ }
436
+ if (
437
+ artifactKeys.some(
438
+ key =>
439
+ !/^[a-f0-9]{64}$/.test(
440
+ String(value.result.artifacts[key]?.artifactHash || '')
441
+ )
442
+ )
443
+ ) {
444
+ return null;
445
+ }
446
+ return cloneJson(value.result);
447
+ }
448
+
449
+ function writeGitArtifactCache(file, result) {
450
+ fs.mkdirSync(path.dirname(file), { recursive: true });
451
+ const tempFile = `${file}.${process.pid}.${Date.now()}.tmp`;
452
+ fs.writeFileSync(
453
+ tempFile,
454
+ `${JSON.stringify(
455
+ {
456
+ schemaVersion: 'git_base_artifacts_v2',
457
+ openxiangdaVersion: OPENXIANGDA_VERSION,
458
+ createdAt: new Date().toISOString(),
459
+ result,
460
+ },
461
+ null,
462
+ 2
463
+ )}\n`,
464
+ { mode: 0o600 }
465
+ );
466
+ fs.renameSync(tempFile, file);
352
467
  }
353
468
 
354
469
  function readBuiltSourceHash(buildRoot, sourceKind, code) {
@@ -1109,22 +1224,6 @@ function splitNullBuffer(buffer) {
1109
1224
  return records;
1110
1225
  }
1111
1226
 
1112
- function normalizeRemoteIdentity(value) {
1113
- const remote = String(value || '').trim();
1114
- try {
1115
- const url = new URL(remote);
1116
- url.username = '';
1117
- url.password = '';
1118
- url.search = '';
1119
- url.hash = '';
1120
- return url.toString();
1121
- } catch {
1122
- return remote
1123
- .replace(/^[^@/]+@([^:]+):/, '$1:')
1124
- .replace(/[?#].*$/, '');
1125
- }
1126
- }
1127
-
1128
1227
  function getChangeBaselineOwnerThreadId(baseline) {
1129
1228
  return String(
1130
1229
  baseline?.workspace?.threadId || baseline?.ownerThreadId || ''