openxiangda 1.0.170 → 1.0.172
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 +10 -9
- package/lib/cli.js +107 -19
- package/lib/release-mainline.js +15 -0
- package/lib/sdd.js +203 -22
- package/lib/workspace-init.js +13 -3
- package/openxiangda-skills/SKILL.md +13 -15
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +10 -13
- package/package.json +1 -1
- package/packages/sdk/dist/build/index.cjs.map +1 -1
- package/packages/sdk/dist/build/index.d.mts +1 -0
- package/packages/sdk/dist/build/index.d.ts +1 -0
- package/packages/sdk/dist/build/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +17 -1
- package/packages/sdk/dist/runtime/react.d.ts +17 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +3 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
- package/templates/openxiangda-react-spa/AGENTS.md +11 -14
- package/templates/openxiangda-react-spa/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +3 -3
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +3 -3
- package/templates/sy-lowcode-app-workspace/AGENTS.md +3 -3
- package/templates/sy-lowcode-app-workspace/app-workspace.config.ts +1 -0
- package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +2 -1
- package/templates/sy-lowcode-app-workspace/src/types/app-workspace.types.ts +1 -0
package/README.md
CHANGED
|
@@ -39,11 +39,12 @@ openxiangda sdd approve add-customer-page --summary "用户确认"
|
|
|
39
39
|
openxiangda sdd verify add-customer-page --changed --stage implementation --profile dev
|
|
40
40
|
openxiangda resource validate function --only customer_get,customer_save --profile dev
|
|
41
41
|
openxiangda resource plan function --only customer_get,customer_save --profile dev
|
|
42
|
-
|
|
43
|
-
openxiangda sdd
|
|
44
|
-
openxiangda release
|
|
45
|
-
openxiangda
|
|
46
|
-
|
|
42
|
+
# merge/push approved task commits to main, then create and push one release bundle
|
|
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
|
|
47
48
|
openxiangda release end --profile dev
|
|
48
49
|
openxiangda resource explain public-access --json
|
|
49
50
|
openxiangda inspect app --profile dev --json
|
|
@@ -56,9 +57,9 @@ User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions
|
|
|
56
57
|
|
|
57
58
|
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
59
|
|
|
59
|
-
|
|
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.
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
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.
|
|
62
63
|
|
|
63
64
|
Source-triggered Function and Automation publishing is source-only by default. Backend Release v2 can mix create, source-only update, and manifest replacement update in one immutable child; a stale target therefore causes zero resource writes instead of failing halfway through 88 Functions and 11 Automations. `--stage-only` stops after verification, reports all handled selectors, and never falls back to direct Function/Automation writes when the platform is missing or incompatible. The release preserves online `resourceBindings`, input/output contracts, descriptive metadata, trigger/view configuration, and enabled/published state, and planned noops do not advance resource versions or timestamps. To intentionally make a complete local manifest replace the online definition, select exact codes and opt in explicitly with `--replace-manifest --reason "<why this replacement is safe>"`; `--sdd-bypass` does not widen that scope. Standard workspaces use the CLI-bundled scoped builder without spawning workspace `pnpm`, and `source_lineage_v1` compares authored source/dependencies independently from generated bundle bytes.
|
|
64
65
|
|
|
@@ -94,9 +95,9 @@ Domestic npm mirrors may lag and return an older OpenXiangda version. `openxiang
|
|
|
94
95
|
|
|
95
96
|
Architecture-class requests are plan-gated by default. For new apps, complex pages, login/register, public/no-login access, role/data-scope design, workflow/automation, App Function, connector, notification, and external integration work, AI agents must plan first and implement only after the user confirms the design.
|
|
96
97
|
|
|
97
|
-
Risk is tiered. Read-only/docs/tests are L0. Narrow reversible
|
|
98
|
+
Risk is tiered. Read-only/docs/tests are L0. Narrow reversible fixes are L1 and use `sdd quick`; L2/L3 retain explicit approval and exact structured coverage. SDD is streamlined by default: incomplete task/evidence/spec prose warns but does not block. Workspaces that intentionally require prose completion may set `strictDocumentation: true`. Actual argv, exact scope, mainline identity, lease, CAS, destructive operations, Secrets, and Root App atomic activation remain hard gates.
|
|
98
99
|
|
|
99
|
-
Every concurrent task
|
|
100
|
+
Every concurrent task uses its own Git worktree/branch and development change. The mainline release coordinator bundles selected approved changes after merge; dependency impact outside the approved scope is reported as a warning rather than silently widening a small release. Live commands still require canonical exact selectors, and app-wide/delete operations need explicit authority.
|
|
100
101
|
|
|
101
102
|
Before confirmation, agents may read, inspect, snapshot, dry-run, ask questions, and output/write the architecture document. They must not edit source files, mutate platform resources, publish, deploy, send notifications, or call live write/delete endpoints.
|
|
102
103
|
|
package/lib/cli.js
CHANGED
|
@@ -105,6 +105,7 @@ const {
|
|
|
105
105
|
approveSddChange,
|
|
106
106
|
buildSddGovernanceReport,
|
|
107
107
|
buildSddStatus,
|
|
108
|
+
createMainlineSddBundle,
|
|
108
109
|
createQuickSddChange,
|
|
109
110
|
getSddChangeScope,
|
|
110
111
|
initSddWorkspace,
|
|
@@ -219,7 +220,7 @@ Usage:
|
|
|
219
220
|
openxiangda open-api spec tags|list|describe [operationId|path] [--method post] [--tag name] [--search text] [--json]
|
|
220
221
|
openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
|
|
221
222
|
openxiangda design gates|template|review [--topic code] [--json]
|
|
222
|
-
openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
223
|
+
openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]
|
|
223
224
|
openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
|
|
224
225
|
openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
|
|
225
226
|
openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
|
|
@@ -287,7 +288,7 @@ Usage:
|
|
|
287
288
|
|
|
288
289
|
OpenXiangda 应用开发使用普通用户登录 token,不需要 AK/SK;外部后端集成使用 open-api skill 与 AK/SK。
|
|
289
290
|
所有直接配置写入必须携带 --change <id>,或先执行 release begin --change <id>;dry-run、schema-plan、validate、query/invoke 等只读/数据面命令不会获取发布租约。
|
|
290
|
-
|
|
291
|
+
正式发布只接受已经合并并 push、且与权威远端默认主分支完全一致的 clean HEAD;激活后直接运行 integration-status 与 release end,不再补做发布后合并。
|
|
291
292
|
表单页、流程表单页和代码页的主链路是 sy-lowcode-app-workspace + openxiangda workspace publish。
|
|
292
293
|
新 React SPA 公开访问使用 public-access 命令和 src/resources/public-access;settings public-access 仅用于旧表单公开设置兼容/修复。
|
|
293
294
|
JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts,代码自动化源码写在 src/automations/<resourceCode>/index.ts。definition-json 中的 sourceFile.localPath 会在 validate/create/publish 时先 TS 校验、再构建并上传为快照。`);
|
|
@@ -644,12 +645,13 @@ async function sdd(args) {
|
|
|
644
645
|
const { flags, positional } = parseArgs(rest);
|
|
645
646
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
646
647
|
print([
|
|
647
|
-
'用法: openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--change id] [--json]',
|
|
648
|
+
'用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--change id] [--json]',
|
|
648
649
|
'常用流程:',
|
|
649
650
|
' openxiangda sdd init --upgrade',
|
|
650
651
|
' openxiangda sdd propose add-customer-list --title "客户列表" --forms customer --pages customer_list --resources',
|
|
651
652
|
' openxiangda sdd quick tune-customer-copy --kind copy --pages customer_list --files src/pages/customer_list/index.tsx --summary "用户确认小范围文案调整"',
|
|
652
653
|
' openxiangda sdd approve add-customer-list --summary "用户确认设计"',
|
|
654
|
+
' openxiangda sdd bundle mainline-release --changes add-customer-list,fix-order-log',
|
|
653
655
|
' openxiangda sdd context --change add-customer-list --json',
|
|
654
656
|
' openxiangda sdd verify --change add-customer-list --stage prepublish --json',
|
|
655
657
|
' openxiangda sdd archive --change add-customer-list',
|
|
@@ -661,7 +663,9 @@ async function sdd(args) {
|
|
|
661
663
|
' - 在 Codex 中 propose/quick/context 会按 CODEX_THREAD_ID 占用当前 worktree;其他任务必须切换到独立 worktree。',
|
|
662
664
|
' - quick 仅用于 L1 精确小改动;Function quick 还必须提供完整 --risk-json,不能用 riskTier 标签绕过。',
|
|
663
665
|
' - verify stage 可选 implementation、prepublish、postpublish、archive;不传保持旧版全量校验。',
|
|
664
|
-
' -
|
|
666
|
+
' - 默认 streamlined 校验只把 approval、精确资源/文件范围、实际发布命令作为硬门禁;任务、证据和规格文案仅告警。配置 strictDocumentation: true 可恢复严格文档门禁。',
|
|
667
|
+
' - bundle 聚合多个 approved change;提交并推送该 bundle 后,在权威主分支执行一次 staged + atomic 发布。',
|
|
668
|
+
' - 新 release 只能从已推送且与权威远端默认主分支一致的 clean HEAD 开始。',
|
|
665
669
|
].join('\n'));
|
|
666
670
|
return;
|
|
667
671
|
}
|
|
@@ -720,6 +724,23 @@ async function sdd(args) {
|
|
|
720
724
|
return;
|
|
721
725
|
}
|
|
722
726
|
|
|
727
|
+
if (subcommand === 'bundle') {
|
|
728
|
+
const changeId = positional[0] || flags.change;
|
|
729
|
+
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
730
|
+
const result = createMainlineSddBundle({
|
|
731
|
+
cwd: process.cwd(),
|
|
732
|
+
changeId,
|
|
733
|
+
title: flags.title || flags.name,
|
|
734
|
+
changes: flags.changes,
|
|
735
|
+
force: Boolean(flags.force),
|
|
736
|
+
});
|
|
737
|
+
if (flags.json) return writeJson(result);
|
|
738
|
+
print(
|
|
739
|
+
`已创建主线发布 bundle: ${result.change.id}\nchanges=${result.bundledChanges.join(', ')}\n目录: ${result.dir}`
|
|
740
|
+
);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
723
744
|
if (subcommand === 'approve') {
|
|
724
745
|
const changeId = positional[0] || flags.change;
|
|
725
746
|
assertOrClaimWorktreeOwner({ cwd: process.cwd(), changeId });
|
|
@@ -837,7 +858,7 @@ async function sdd(args) {
|
|
|
837
858
|
return;
|
|
838
859
|
}
|
|
839
860
|
|
|
840
|
-
fail('用法: openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--json]');
|
|
861
|
+
fail('用法: openxiangda sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive [change] [--json]');
|
|
841
862
|
}
|
|
842
863
|
|
|
843
864
|
function parseSddAffectedFlags(flags) {
|
|
@@ -886,8 +907,8 @@ async function release(args) {
|
|
|
886
907
|
' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
|
|
887
908
|
' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
|
|
888
909
|
' - 默认 TTL 为 1800 秒;长异步发布每 30 秒检查并在到期前自动续租,续租失败会以 PUBLISH_LEASE_LOST 阻断后续写入。',
|
|
889
|
-
' - begin
|
|
890
|
-
' -
|
|
910
|
+
' - begin 仅接受 clean 且与权威远端默认 main/master 完全一致的 HEAD;feature branch 先合并并 push,再由主分支一次发布。',
|
|
911
|
+
' - 因发布源已在远端主线,成功激活后 integration-status 会立即通过,end 不再等待补合并。',
|
|
891
912
|
' - integration-status --check 可作为 CI 门禁;尚未回合时返回非零。',
|
|
892
913
|
' - 主线目标只能来自权威 Git remote 的实时默认 main/master;参数和环境变量不能改写。',
|
|
893
914
|
' - 租约过期也会保留待回合证据;完成 merge/push 后仍可执行 end 清理。',
|
|
@@ -9120,7 +9141,7 @@ async function runtime(args) {
|
|
|
9120
9141
|
' - runtime 只发布前端产物;表单、流程、通知等资源仍走 resource publish。',
|
|
9121
9142
|
' - deploy/activate 会读取当前 Runtime head 并携带 CAS preconditions;head 已变化时平台返回 409,避免静默覆盖。',
|
|
9122
9143
|
' - 所有 deploy(含 --no-activate)先获取应用 publish lease;在任何构建/上传前要求 Git clean committed HEAD,并校验当前 HEAD 继承线上 Runtime 源码血缘。',
|
|
9123
|
-
' - release begin
|
|
9144
|
+
' - release begin 只接受与权威远端默认主分支完全一致的 clean HEAD;先合并/push 所有 approved changes,再一次发布。',
|
|
9124
9145
|
' - Runtime 同步构建前把 lease 续到平台最大 TTL;构建/上传后每次写入仍会重新校验,失租不会继续激活。',
|
|
9125
9146
|
' - build 不能安全前移到 lease 之前:直传模式的 assetBase 由受保护的 upload-plan 返回,不能用 gateway 路径静默替换。',
|
|
9126
9147
|
' - deploy 会输出 lease/head/preflight/build/upload/activate 阶段进度,JSON 结果包含 timings。',
|
|
@@ -9190,6 +9211,9 @@ async function runtime(args) {
|
|
|
9190
9211
|
}
|
|
9191
9212
|
|
|
9192
9213
|
if (subcommand === 'deploy') {
|
|
9214
|
+
const runtimeBuildCommand = flags['no-build']
|
|
9215
|
+
? null
|
|
9216
|
+
: assertRuntimeBuildReady(flags['build-command']);
|
|
9193
9217
|
const scopedRuntimeFiles = flags.change
|
|
9194
9218
|
? buildWorkspaceReleasePlan({
|
|
9195
9219
|
changeId: flags.change,
|
|
@@ -9324,7 +9348,7 @@ async function runtime(args) {
|
|
|
9324
9348
|
buildId,
|
|
9325
9349
|
appType: target.appType,
|
|
9326
9350
|
assetBaseUrl,
|
|
9327
|
-
command:
|
|
9351
|
+
command: runtimeBuildCommand,
|
|
9328
9352
|
jsonOutput: Boolean(flags.json),
|
|
9329
9353
|
})
|
|
9330
9354
|
);
|
|
@@ -9630,9 +9654,34 @@ function runRuntimeBuild(options) {
|
|
|
9630
9654
|
if (result.status !== 0) fail(`runtime build 失败: exit ${result.status}`);
|
|
9631
9655
|
}
|
|
9632
9656
|
|
|
9657
|
+
function assertRuntimeBuildReady(explicitCommand) {
|
|
9658
|
+
if (explicitCommand) return String(explicitCommand);
|
|
9659
|
+
const packageFile = path.join(process.cwd(), 'package.json');
|
|
9660
|
+
if (!fs.existsSync(packageFile)) {
|
|
9661
|
+
fail('RUNTIME_BUILD_NOT_READY: 缺少 package.json;尚未获取发布租约');
|
|
9662
|
+
}
|
|
9663
|
+
let packageJson;
|
|
9664
|
+
try {
|
|
9665
|
+
packageJson = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
9666
|
+
} catch (error) {
|
|
9667
|
+
fail(`RUNTIME_BUILD_NOT_READY: package.json 无法解析: ${error.message}`);
|
|
9668
|
+
}
|
|
9669
|
+
if (!packageJson?.scripts?.build) {
|
|
9670
|
+
fail('RUNTIME_BUILD_NOT_READY: package.json 缺少 scripts.build;尚未获取发布租约');
|
|
9671
|
+
}
|
|
9672
|
+
const modulesDir = path.join(process.cwd(), 'node_modules');
|
|
9673
|
+
if (!fs.existsSync(modulesDir)) {
|
|
9674
|
+
fail(
|
|
9675
|
+
'RUNTIME_BUILD_DEPENDENCIES_MISSING: 缺少 node_modules;请在开发阶段完成依赖安装,发布命令不会临时安装或修改锁文件'
|
|
9676
|
+
);
|
|
9677
|
+
}
|
|
9678
|
+
return defaultRuntimeBuildCommand();
|
|
9679
|
+
}
|
|
9680
|
+
|
|
9633
9681
|
function defaultRuntimeBuildCommand() {
|
|
9634
|
-
|
|
9635
|
-
|
|
9682
|
+
// npm run only executes the already-declared script and resolves
|
|
9683
|
+
// node_modules/.bin. Unlike pnpm build, it does not reject a valid worktree
|
|
9684
|
+
// whose node_modules is a symlink into the primary checkout.
|
|
9636
9685
|
return 'npm run build';
|
|
9637
9686
|
}
|
|
9638
9687
|
|
|
@@ -10130,7 +10179,7 @@ async function commands(args) {
|
|
|
10130
10179
|
'open-api spec tags|list|describe [operationId|path]',
|
|
10131
10180
|
'open-api credential list|get|create|update|rotate-secret [id]',
|
|
10132
10181
|
'design gates|template|review [--topic code]',
|
|
10133
|
-
'sdd init|migrate|propose|approve|status|context|verify|sync|archive',
|
|
10182
|
+
'sdd init|migrate|propose|quick|bundle|approve|status|context|verify|sync|archive',
|
|
10134
10183
|
'release begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
|
|
10135
10184
|
'env',
|
|
10136
10185
|
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
@@ -10168,7 +10217,7 @@ async function commands(args) {
|
|
|
10168
10217
|
'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
|
|
10169
10218
|
'App Function Secret values never belong in a manifest or workspace file. Use top-level function secretRefs, ctx.secrets.get(name), and secret create/rotate via hidden TTY or --value-stdin.',
|
|
10170
10219
|
'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change with coverage.json/release.json verification unless --sdd-bypass --reason is used.',
|
|
10171
|
-
'A formal release
|
|
10220
|
+
'A formal release only starts from a clean local main/master that exactly equals the authoritative remote tip. Merge and push approved changes first; after activation verify release integration-status, then run release end without a post-release merge.',
|
|
10172
10221
|
],
|
|
10173
10222
|
};
|
|
10174
10223
|
if (flags.json) return writeJson(manifest);
|
|
@@ -20849,9 +20898,9 @@ function getChangeScopedWorkspaceFiles(changeScope, runtimeMode, options = {}) {
|
|
|
20849
20898
|
);
|
|
20850
20899
|
};
|
|
20851
20900
|
return unique([
|
|
20852
|
-
...(changeScope.ownedFiles || []),
|
|
20853
20901
|
...unique([
|
|
20854
20902
|
...(changeScope.recordedChangedFiles || []),
|
|
20903
|
+
...(changeScope.ownedFiles || []),
|
|
20855
20904
|
...dirtyFiles,
|
|
20856
20905
|
]).filter(file =>
|
|
20857
20906
|
fileBelongsToChangeScope(file, changeScope, runtimeMode) || dependencyBelongsToScope(file)
|
|
@@ -21042,17 +21091,47 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21042
21091
|
runtime: Boolean(changeScope.targets.runtime),
|
|
21043
21092
|
other: [...(changeScope.targets.other || [])],
|
|
21044
21093
|
}
|
|
21045
|
-
|
|
21094
|
+
: classifyWorkspaceFiles(files, runtimeMode, {
|
|
21046
21095
|
baseRef: options.since || 'HEAD',
|
|
21047
21096
|
});
|
|
21097
|
+
let dependencyImpact = {};
|
|
21048
21098
|
if (changeScope) {
|
|
21049
21099
|
const dependencyTargets = getWorkspaceImpactedResourceTargets(files);
|
|
21100
|
+
dependencyImpact = Object.fromEntries(
|
|
21101
|
+
['functions', 'automations', 'workflows', 'jsCodeNodes'].map(key => [
|
|
21102
|
+
key,
|
|
21103
|
+
(dependencyTargets[key] || []).filter(
|
|
21104
|
+
code => !(targets[key] || []).includes(code)
|
|
21105
|
+
),
|
|
21106
|
+
])
|
|
21107
|
+
);
|
|
21108
|
+
// The approved structured scope is authoritative. Shared-source impact is
|
|
21109
|
+
// reported for review, but no longer silently broadens a release into
|
|
21110
|
+
// dozens of unrelated resources. Callers may opt in when the intended
|
|
21111
|
+
// change genuinely requires the whole dependency closure.
|
|
21112
|
+
if (options.expandDependencies) {
|
|
21050
21113
|
for (const key of ['functions', 'automations', 'workflows', 'jsCodeNodes']) {
|
|
21051
21114
|
targets[key] = unique([...(targets[key] || []), ...(dependencyTargets[key] || [])]).sort();
|
|
21052
21115
|
}
|
|
21116
|
+
}
|
|
21053
21117
|
}
|
|
21054
21118
|
if (options.includeResources) targets.resources = true;
|
|
21055
|
-
const issues =
|
|
21119
|
+
const issues = [
|
|
21120
|
+
...scanWorkspaceReliabilityIssues(files),
|
|
21121
|
+
...Object.entries(dependencyImpact).flatMap(([key, codes]) =>
|
|
21122
|
+
codes.length > 0
|
|
21123
|
+
? [
|
|
21124
|
+
{
|
|
21125
|
+
rule: 'shared-source-outside-approved-scope',
|
|
21126
|
+
severity: 'warn',
|
|
21127
|
+
file: '(release scope)',
|
|
21128
|
+
line: 0,
|
|
21129
|
+
message: `${key} shared-source impact not included unless explicitly approved: ${codes.join(', ')}`,
|
|
21130
|
+
},
|
|
21131
|
+
]
|
|
21132
|
+
: []
|
|
21133
|
+
),
|
|
21134
|
+
];
|
|
21056
21135
|
const errors = issues
|
|
21057
21136
|
.filter(issue => issue.severity === 'error')
|
|
21058
21137
|
.map(issue => ({
|
|
@@ -21065,11 +21144,19 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21065
21144
|
options.profile,
|
|
21066
21145
|
changeScope?.changeId
|
|
21067
21146
|
);
|
|
21147
|
+
const knownTargetCount =
|
|
21148
|
+
targets.forms.length +
|
|
21149
|
+
targets.pages.length +
|
|
21150
|
+
(targets.functions || []).length +
|
|
21151
|
+
(targets.automations || []).length +
|
|
21152
|
+
(targets.workflows || []).length +
|
|
21153
|
+
(targets.jsCodeNodes || []).length +
|
|
21154
|
+
(targets.runtime ? 1 : 0);
|
|
21068
21155
|
const commands =
|
|
21069
|
-
changeScope?.commands?.length > 0
|
|
21156
|
+
targets.resources && knownTargetCount === 0 && changeScope?.commands?.length > 0
|
|
21070
21157
|
? [...changeScope.commands]
|
|
21071
21158
|
: generatedCommands;
|
|
21072
|
-
if (
|
|
21159
|
+
if (commands.length === generatedCommands.length && targets.resources && knownTargetCount === 0) {
|
|
21073
21160
|
errors.push({
|
|
21074
21161
|
name: 'sdd-release-generic-resource-selector-required',
|
|
21075
21162
|
message:
|
|
@@ -21077,7 +21164,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21077
21164
|
});
|
|
21078
21165
|
}
|
|
21079
21166
|
if (
|
|
21080
|
-
|
|
21167
|
+
commands.length === generatedCommands.length &&
|
|
21081
21168
|
(targets.jsCodeNodes || []).length > 0 &&
|
|
21082
21169
|
(targets.functions || []).length === 0 &&
|
|
21083
21170
|
(targets.automations || []).length === 0 &&
|
|
@@ -21147,6 +21234,7 @@ function buildWorkspaceReleasePlan(options = {}) {
|
|
|
21147
21234
|
checks,
|
|
21148
21235
|
sdd,
|
|
21149
21236
|
issues,
|
|
21237
|
+
dependencyImpact,
|
|
21150
21238
|
errors: [...errors, ...sddErrors],
|
|
21151
21239
|
};
|
|
21152
21240
|
}
|
package/lib/release-mainline.js
CHANGED
|
@@ -35,6 +35,20 @@ function prepareReleaseSourceRevision(options = {}) {
|
|
|
35
35
|
mainline.remoteName,
|
|
36
36
|
mainline.branchName
|
|
37
37
|
);
|
|
38
|
+
if (branch !== mainline.branchName) {
|
|
39
|
+
throw releaseMainlineError(
|
|
40
|
+
'RELEASE_SOURCE_MAINLINE_REQUIRED',
|
|
41
|
+
`正式发布只能从权威主分支 ${formatMainline(mainline)} 执行;当前分支为 ${branch}。请先把已批准变更合并到主分支并切换过去`,
|
|
42
|
+
{ branch, mainline }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (sourceRevision.baseCommit !== mainline.tipCommit) {
|
|
46
|
+
throw releaseMainlineError(
|
|
47
|
+
'RELEASE_SOURCE_MAINLINE_NOT_PUSHED',
|
|
48
|
+
`本地主分支 ${shortCommit(sourceRevision.baseCommit)} 尚未与远端 ${formatMainline(mainline)}@${shortCommit(mainline.tipCommit)} 完全一致;请先 fast-forward/push,再从该提交一次发布`,
|
|
49
|
+
{ sourceRevision, mainline }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
38
52
|
if (
|
|
39
53
|
!isGitAncestor(cwd, mainline.tipCommit, sourceRevision.baseCommit, {
|
|
40
54
|
remoteName: mainline.remoteName,
|
|
@@ -57,6 +71,7 @@ function prepareReleaseSourceRevision(options = {}) {
|
|
|
57
71
|
mainBranch: mainline.branchName,
|
|
58
72
|
mainTipCommit: mainline.tipCommit,
|
|
59
73
|
mainlineSource: mainline.source,
|
|
74
|
+
mainlinePolicy: 'publish-from-main-v1',
|
|
60
75
|
capturedAt: new Date().toISOString(),
|
|
61
76
|
};
|
|
62
77
|
}
|