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 +4 -7
- package/lib/change-baseline.js +117 -18
- package/lib/cli.js +695 -102
- package/lib/form-schema-loader.js +166 -0
- package/lib/git-remote-identity.js +89 -0
- package/lib/release-mainline.js +46 -24
- package/lib/release-plan.js +294 -0
- package/lib/runtime-lineage.js +23 -3
- package/lib/sdd.js +178 -41
- package/openxiangda-skills/SKILL.md +4 -6
- package/openxiangda-skills/references/openxiangda-api.md +6 -6
- package/openxiangda-skills/references/pages/page-sdk.md +1 -1
- package/openxiangda-skills/references/permissions-settings.md +4 -3
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +4 -6
- package/openxiangda-skills/skills/openxiangda-permission-settings/SKILL.md +5 -4
- package/package.json +2 -1
- package/packages/sdk/dist/runtime/react.d.mts +3 -0
- package/packages/sdk/dist/runtime/react.d.ts +3 -0
- package/templates/openxiangda-react-spa/AGENTS.md +5 -11
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +2 -8
- package/templates/sy-lowcode-app-workspace/AGENTS.md +2 -2
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
|
-
|
|
45
|
-
openxiangda release
|
|
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
|
|
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
|
|
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
|
|
package/lib/change-baseline.js
CHANGED
|
@@ -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:${
|
|
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
|
-
|
|
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 || ''
|