openxiangda 1.0.213 → 1.0.215
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 +6 -2
- package/lib/application-environments.js +227 -3
- package/lib/cli.js +1521 -136
- package/lib/policy.js +1 -0
- package/lib/release-mainline.js +67 -8
- package/lib/resource-binding-contract.js +210 -0
- package/lib/sdd.js +216 -74
- package/lib/workspace-init.js +9 -3
- package/lib/worktree-cleanup.js +631 -0
- package/lib/worktree-owner.js +121 -5
- package/openxiangda-skills/SKILL.md +11 -4
- package/openxiangda-skills/references/connector-resources.md +1 -1
- package/openxiangda-skills/references/openxiangda-api.md +1 -1
- package/openxiangda-skills/references/workflow-v3.md +1 -1
- package/openxiangda-skills/references/workspace-state.md +1 -1
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +9 -1
- package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
- package/package.json +3 -1
- package/packages/sdk/dist/runtime/index.cjs.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.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +37 -1
- package/packages/sdk/dist/runtime/react.d.ts +37 -1
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +6 -0
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +6 -0
- package/templates/openxiangda-react-spa/AGENTS.md +8 -1
- package/templates/openxiangda-react-spa/scripts/deploy.mjs +1 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +6 -1
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +6 -0
- package/templates/sy-lowcode-app-workspace/AGENTS.md +9 -1
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ openxiangda app snapshot APP_XXXX --profile dev --json
|
|
|
82
82
|
|
|
83
83
|
User tokens are stored in `~/.openxiangda/profiles.json` with `0600` permissions. Shared workspace environment values, including `APP_OSS_*`, can live in `~/.openxiangda/.env` and are inherited by new workspaces. Project `.env` files still work and override the global defaults. Project state is stored in `.openxiangda/state.json` and contains only durable profile-specific resource IDs and environment bindings; volatile candidate/deployment progress lives in the private `.openxiangda/releases/` journal so a release does not dirty the reviewed Git state. Durable CLI writes use a workspace lock plus atomic merge/rename so concurrent processes do not truncate another profile's state.
|
|
84
84
|
|
|
85
|
-
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship`
|
|
85
|
+
An environment-managed workspace keeps one logical application with independent `preproduction` and `production` targets. Each target owns its own `appType`, resource IDs, release heads, data, and side-effect policy; IDs must never be copied across targets. `release ship` always executes the same ordered candidate → preproduction → production protocol. The normal first invocation seals the candidate, deploys only to preproduction, and stops at `awaiting_production_confirmation`; a later invocation with `--confirm-production` promotes it. For an explicitly authorized emergency, supplying `--confirm-production` on the first invocation runs both phases in one command without bypassing preproduction, CAS, evidence, or production confirmation. Candidate sealing covers source, `public/`, build controls/scripts, stable environment/resource bindings, and target-specific hashed Runtime artifacts. Both deployments upload those artifacts with `--no-build`; each deployment is completed with evidence and reaches terminal `succeeded`, so it cannot leave the target slot blocked. Unrelated commits may land on authoritative mainline between phases only while the sealed commit remains an ancestor and every sealed input/binding/artifact still validates. Real human acceptance remains the recommended default and `--acceptance-note` records it. For audited historical-lineage adoption or reviewed Backend manifest replacement, the existing paired flags and exact-scope gates remain mandatory. Supported configuration resources use exact `resourceSelectors`; unknown, wildcard, destructive, and genuinely unscoped generic resources remain blocked. Lower-level candidate/deploy/test/promote commands are recovery primitives. Direct `release publish` is retained only for legacy unmanaged workspaces.
|
|
86
86
|
|
|
87
87
|
Exact, non-destructive configuration selectors such as Data Views and permission groups are now sequenced automatically inside the same ship journal instead of requiring separate SDD changes. New forms are idempotently ensured per environment before their immutable FormRelease is staged. Unscoped resources and destructive configuration deletes remain fail-closed.
|
|
88
88
|
|
|
@@ -108,6 +108,8 @@ Parallel tasks develop and test in isolated worktrees, but feature worktrees do
|
|
|
108
108
|
|
|
109
109
|
Because promotion starts from an already-pushed authoritative mainline commit, `openxiangda release integration-status --change <change> --profile <name>` should pass immediately after activation. After a managed ship has ended, `--change` recovers lineage directly from the private `ship.json` or follows its production/preproduction deployment IDs to the matching `execution.json`; a recovery failure names the missing file or field. Run it and `release end`; there is no post-release merge step.
|
|
110
110
|
|
|
111
|
+
Task worktrees are disposable only after the release lifecycle is closed. From the synchronized canonical main checkout, run `openxiangda workspace cleanup` for a dry-run inventory. A worktree is `SAFE` only when it is OpenXiangda/Codex-managed, its commit is contained in live remote mainline, it is clean and unowned, and it has no private candidate/release journal or promotion lease/baseline. `--apply` is bound to the exact hashed dry-run plan, revalidates under the worktree owner lock, and removes only those reviewed entries plus their unchanged local branches. Newly-safe worktrees are left for the next review. Malformed owner state fails closed. Stale records are reported for explicit manual review because the CLI never runs repository-wide `git worktree prune`. Remote branches are retained.
|
|
112
|
+
|
|
111
113
|
`openxiangda task status --change <id>` is the compact, read-only answer to
|
|
112
114
|
“现在到哪一步”:it combines SDD、TaskResult、IntegrationBundle、execution
|
|
113
115
|
journal、lease 和 post-commit outbox,输出进度、ETA、是否已经发生平台写入、
|
|
@@ -175,6 +177,8 @@ L1 quick records are generated in a compact form and should not be expanded into
|
|
|
175
177
|
|
|
176
178
|
Every concurrent task uses its own Git worktree/branch and development change. The canonical main checkout is reserved for integration and release; tasks never stash/restore each other's files to make it publishable. 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.
|
|
177
179
|
|
|
180
|
+
After the mainline release and `release end`, the coordinator runs `workspace cleanup` from canonical main and applies only `SAFE` entries. Dirty/unmerged/owned worktrees and worktrees with private release or promotion state remain blocked for recovery.
|
|
181
|
+
|
|
178
182
|
Agent investigation is bounded to one complete CodeGraph survey plus at most one focused follow-up, and ordinary work loads one domain skill. Unchanged lease/build waits use `task status --watch` and produce updates only on material transitions.
|
|
179
183
|
|
|
180
184
|
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.
|
|
@@ -502,7 +506,7 @@ const PublicAccessError = ({ error }: { error: { message?: string } }) => (
|
|
|
502
506
|
|
|
503
507
|
多表只读查询和固定口径统计优先声明 `src/resources/data-views/*.json` 数据视图,而不是在页面里手写多次单表查询再拼数据。默认 `storageMode: "materialized"` 会创建 PostgreSQL materialized view,适合读多写少和可接受刷新延迟的列表/报表;`storageMode: "live"` 每次查询实时编译逻辑视图,适合强实时但数据量可控的复杂查询。`viewType: "aggregate"` 是统计聚合视图,适合按客户、状态、月份等维度聚合 count/sum/avg/min/max。发布时 CLI 会把 `formCode` 解析为当前 profile 的 `formUuid`;页面通过 `sdk.dataView.query(code, params)` 查询行级视图,通过 `sdk.dataView.stats(code, params)` 查询聚合视图,也可以用 `sdk.dataSource.run()` 路由 `dataView.query` / `dataView.stats`。materialized 模式应为常用筛选、排序、统计维度和时间桶声明 `indexes`,并确认用户能接受的刷新延迟;live 模式忽略 `indexes`,不需要刷新。
|
|
504
508
|
|
|
505
|
-
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.roles`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function
|
|
509
|
+
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form.queryOne/queryMany/getById/createOne/updateOne/updateById`、`ctx.process.startFromExistingInstance/resolveCapabilities/resubmitTask/withdraw/transferTask`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.organization`、`ctx.platform.roles`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。`ctx.process` 复用正式工作流服务,以真实运行时 operator 执行,并严格限制在当前应用及 Function `resources.forms` 已绑定的流程表单;流程实例、任务授权、操作日志、事件和 replay 语义与 PageSdk 后端接口一致。角色查询和角色成员维护优先使用 `ctx.platform.roles.list/findByCode/addUsers/removeUser`,底层 `ctx.platform.api` 返回 HTTP 包装和平台 envelope,需要业务代码自行解包。运行时页面调用会在 `ctx.operator`、`ctx.currentUser`、`ctx.permissions` 中注入可信的 `roleCodes`、`currentRoleCode`、`hasFullAccess`、`isAppAdmin`、`isPlatformAdmin`,敏感动作必须读取这些服务端上下文做授权,不要信任页面 input 里传入的角色字段。`ctx.form.createOne/updateOne/updateById` 是后端受控写入,授权边界是函数 manifest 的 `resources.forms` 绑定和函数调用授权,不是页面用户对目标表单的直接提交入口;报名、签到、福利选择等内部多表写入应走 App Function,不要为了写内部表单给普通用户开放原始 submit 权限。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。直接运行时接口默认要求调用者具备应用自动化管理权限;普通页面调用必须在函数 `definitionJson.runtimeInvoke.audience` 声明 `authenticated`、`page_permission_group`、`app_roles` 或 `scope_policy`,`roleCodes` 只用于当前应用角色精确匹配,不支持 `"*"` / `"all-app-roles"`。自动化/流程内部调用走服务端受控上下文。内部业务表单需要关闭原始写入接口时,在表单 settings 中设置 `runtimeWrite.mode="function_only"`。
|
|
506
510
|
|
|
507
511
|
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 会失败关闭,绝不忽略绑定。
|
|
508
512
|
|
|
@@ -570,20 +570,163 @@ function hashWorkspaceFile(cwd, relativePath) {
|
|
|
570
570
|
if (
|
|
571
571
|
!normalized ||
|
|
572
572
|
relative.startsWith('..') ||
|
|
573
|
-
path.isAbsolute(relative)
|
|
574
|
-
!fs.existsSync(absolute) ||
|
|
575
|
-
!fs.statSync(absolute).isFile()
|
|
573
|
+
path.isAbsolute(relative)
|
|
576
574
|
) {
|
|
577
575
|
return null;
|
|
578
576
|
}
|
|
577
|
+
if (!fs.existsSync(absolute)) {
|
|
578
|
+
return {
|
|
579
|
+
path: normalized,
|
|
580
|
+
exists: false,
|
|
581
|
+
size: 0,
|
|
582
|
+
sha256: null,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (!fs.statSync(absolute).isFile()) return null;
|
|
579
586
|
const buffer = fs.readFileSync(absolute);
|
|
580
587
|
return {
|
|
581
588
|
path: normalized,
|
|
589
|
+
exists: true,
|
|
582
590
|
size: buffer.length,
|
|
583
591
|
sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
|
|
584
592
|
};
|
|
585
593
|
}
|
|
586
594
|
|
|
595
|
+
function buildCandidateEnvironmentBindings(state) {
|
|
596
|
+
const targets =
|
|
597
|
+
state?.targets && typeof state.targets === 'object'
|
|
598
|
+
? state.targets
|
|
599
|
+
: {};
|
|
600
|
+
return Object.fromEntries(
|
|
601
|
+
Object.entries(targets)
|
|
602
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
603
|
+
.map(([targetName, binding]) => [
|
|
604
|
+
targetName,
|
|
605
|
+
{
|
|
606
|
+
targetName,
|
|
607
|
+
profile: binding?.profile || null,
|
|
608
|
+
environmentId: binding?.environmentId || null,
|
|
609
|
+
kind: binding?.kind || null,
|
|
610
|
+
appType: binding?.appType || null,
|
|
611
|
+
publicOrigin: binding?.publicOrigin || null,
|
|
612
|
+
sideEffectPolicy:
|
|
613
|
+
cloneJsonValue(binding?.sideEffectPolicy || {}) || {},
|
|
614
|
+
revision:
|
|
615
|
+
binding?.revision === undefined
|
|
616
|
+
? null
|
|
617
|
+
: binding.revision,
|
|
618
|
+
resources: cloneJsonValue(binding?.resources || {}) || {},
|
|
619
|
+
},
|
|
620
|
+
])
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function candidateBindingValueContains(expected, current) {
|
|
625
|
+
if (Array.isArray(expected)) {
|
|
626
|
+
return (
|
|
627
|
+
Array.isArray(current) &&
|
|
628
|
+
expected.length === current.length &&
|
|
629
|
+
expected.every((item, index) =>
|
|
630
|
+
candidateBindingValueContains(item, current[index])
|
|
631
|
+
)
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (expected && typeof expected === 'object') {
|
|
635
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
return Object.entries(expected).every(([key, value]) =>
|
|
639
|
+
candidateBindingValueContains(value, current[key])
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return expected === current;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function inspectCandidateEnvironmentBindings(candidate, state) {
|
|
646
|
+
const expected =
|
|
647
|
+
candidate?.sourceBundleManifest?.environmentBindings ||
|
|
648
|
+
candidate?.environmentBindings ||
|
|
649
|
+
{};
|
|
650
|
+
const current = buildCandidateEnvironmentBindings(state);
|
|
651
|
+
const mismatches = [];
|
|
652
|
+
const expectedNames = Object.keys(expected).sort();
|
|
653
|
+
const currentNames = Object.keys(current).sort();
|
|
654
|
+
if (canonicalJson(expectedNames) !== canonicalJson(currentNames)) {
|
|
655
|
+
mismatches.push({
|
|
656
|
+
targetName: '(targets)',
|
|
657
|
+
reason: 'target-set-changed',
|
|
658
|
+
expected: expectedNames,
|
|
659
|
+
current: currentNames,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
for (const targetName of expectedNames) {
|
|
663
|
+
const expectedBinding = expected[targetName];
|
|
664
|
+
const currentBinding = current[targetName];
|
|
665
|
+
if (!currentBinding) continue;
|
|
666
|
+
for (const field of [
|
|
667
|
+
'targetName',
|
|
668
|
+
'profile',
|
|
669
|
+
'environmentId',
|
|
670
|
+
'kind',
|
|
671
|
+
'appType',
|
|
672
|
+
'publicOrigin',
|
|
673
|
+
'sideEffectPolicy',
|
|
674
|
+
'revision',
|
|
675
|
+
]) {
|
|
676
|
+
if (
|
|
677
|
+
canonicalJson(expectedBinding?.[field]) !==
|
|
678
|
+
canonicalJson(currentBinding?.[field])
|
|
679
|
+
) {
|
|
680
|
+
mismatches.push({
|
|
681
|
+
targetName,
|
|
682
|
+
field,
|
|
683
|
+
reason: 'changed',
|
|
684
|
+
expected: expectedBinding?.[field],
|
|
685
|
+
current: currentBinding?.[field],
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (
|
|
690
|
+
!candidateBindingValueContains(
|
|
691
|
+
expectedBinding?.resources || {},
|
|
692
|
+
currentBinding.resources || {}
|
|
693
|
+
)
|
|
694
|
+
) {
|
|
695
|
+
mismatches.push({
|
|
696
|
+
targetName,
|
|
697
|
+
field: 'resources',
|
|
698
|
+
reason: 'sealed-resource-binding-changed',
|
|
699
|
+
expected: expectedBinding?.resources || {},
|
|
700
|
+
current: currentBinding.resources || {},
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
valid: mismatches.length === 0,
|
|
706
|
+
checked: expectedNames.length,
|
|
707
|
+
mismatches,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function assertCandidateEnvironmentBindingsCurrent(candidate, state) {
|
|
712
|
+
const inspection = inspectCandidateEnvironmentBindings(candidate, state);
|
|
713
|
+
if (inspection.valid) return inspection;
|
|
714
|
+
const error = new Error(
|
|
715
|
+
`CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED: sealed candidate 的环境或资源绑定已变化: ${inspection.mismatches
|
|
716
|
+
.slice(0, 8)
|
|
717
|
+
.map(
|
|
718
|
+
item =>
|
|
719
|
+
`${item.targetName}${
|
|
720
|
+
item.field ? `.${item.field}` : ''
|
|
721
|
+
}(${item.reason})`
|
|
722
|
+
)
|
|
723
|
+
.join(', ')}`
|
|
724
|
+
);
|
|
725
|
+
error.code = 'CANDIDATE_ENVIRONMENT_BINDINGS_CHANGED';
|
|
726
|
+
error.details = inspection;
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
|
|
587
730
|
function buildCandidateBundle(input) {
|
|
588
731
|
const cwd = path.resolve(input.cwd || process.cwd());
|
|
589
732
|
const files = Array.from(new Set(input.files || []))
|
|
@@ -610,10 +753,17 @@ function buildCandidateBundle(input) {
|
|
|
610
753
|
sourceRevision: input.sourceRevision,
|
|
611
754
|
runtimeMode: input.runtimeMode,
|
|
612
755
|
targets: input.targets,
|
|
756
|
+
inputPolicy: 'candidate-inputs-v2',
|
|
613
757
|
files,
|
|
758
|
+
environmentBindings:
|
|
759
|
+
cloneJsonValue(input.environmentBindings || {}) || {},
|
|
760
|
+
runtimeArtifacts:
|
|
761
|
+
cloneJsonValue(input.runtimeArtifacts || []) || [],
|
|
614
762
|
testPlanHash: sha256Canonical(testPlan),
|
|
615
763
|
prepublishVerificationHash:
|
|
616
764
|
input.prepublishVerificationHash || null,
|
|
765
|
+
candidatePreflightHash:
|
|
766
|
+
input.candidatePreflightHash || null,
|
|
617
767
|
openxiangdaVersion: input.openxiangdaVersion,
|
|
618
768
|
};
|
|
619
769
|
return {
|
|
@@ -624,6 +774,75 @@ function buildCandidateBundle(input) {
|
|
|
624
774
|
};
|
|
625
775
|
}
|
|
626
776
|
|
|
777
|
+
function inspectCandidateBundleFiles(candidate, cwd = process.cwd()) {
|
|
778
|
+
const manifest =
|
|
779
|
+
candidate?.sourceBundleManifest &&
|
|
780
|
+
typeof candidate.sourceBundleManifest === 'object'
|
|
781
|
+
? candidate.sourceBundleManifest
|
|
782
|
+
: candidate;
|
|
783
|
+
const expectedFiles = Array.isArray(manifest?.files)
|
|
784
|
+
? manifest.files
|
|
785
|
+
: [];
|
|
786
|
+
const mismatches = [];
|
|
787
|
+
const seen = new Set();
|
|
788
|
+
for (const expected of expectedFiles) {
|
|
789
|
+
const file = String(expected?.path || '').replace(/\\/g, '/');
|
|
790
|
+
if (!file || seen.has(file)) {
|
|
791
|
+
mismatches.push({
|
|
792
|
+
path: file || '(missing path)',
|
|
793
|
+
reason: seen.has(file) ? 'duplicate' : 'invalid',
|
|
794
|
+
});
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
seen.add(file);
|
|
798
|
+
const current = hashWorkspaceFile(path.resolve(cwd), file);
|
|
799
|
+
const expectedExists = expected.exists !== false;
|
|
800
|
+
if (!current || (expectedExists && current.exists === false)) {
|
|
801
|
+
mismatches.push({ path: file, reason: 'missing' });
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (!expectedExists) {
|
|
805
|
+
if (current.exists !== false) {
|
|
806
|
+
mismatches.push({ path: file, reason: 'reappeared', current });
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (
|
|
811
|
+
current.size !== Number(expected.size) ||
|
|
812
|
+
current.sha256 !== String(expected.sha256 || '').toLowerCase()
|
|
813
|
+
) {
|
|
814
|
+
mismatches.push({
|
|
815
|
+
path: file,
|
|
816
|
+
reason: 'changed',
|
|
817
|
+
expected: {
|
|
818
|
+
size: Number(expected.size),
|
|
819
|
+
sha256: String(expected.sha256 || '').toLowerCase(),
|
|
820
|
+
},
|
|
821
|
+
current,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
valid: mismatches.length === 0,
|
|
827
|
+
checked: expectedFiles.length,
|
|
828
|
+
mismatches,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function assertCandidateBundleFilesCurrent(candidate, cwd = process.cwd()) {
|
|
833
|
+
const inspection = inspectCandidateBundleFiles(candidate, cwd);
|
|
834
|
+
if (inspection.valid) return inspection;
|
|
835
|
+
const error = new Error(
|
|
836
|
+
`CANDIDATE_INPUTS_CHANGED: sealed candidate 的发布输入已变化: ${inspection.mismatches
|
|
837
|
+
.slice(0, 8)
|
|
838
|
+
.map(item => `${item.path}(${item.reason})`)
|
|
839
|
+
.join(', ')}`
|
|
840
|
+
);
|
|
841
|
+
error.code = 'CANDIDATE_INPUTS_CHANGED';
|
|
842
|
+
error.details = inspection;
|
|
843
|
+
throw error;
|
|
844
|
+
}
|
|
845
|
+
|
|
627
846
|
function releaseExecutionPath(changeId, deploymentId, cwd = process.cwd()) {
|
|
628
847
|
const base = path.join(
|
|
629
848
|
cwd,
|
|
@@ -706,7 +925,10 @@ function isRecoverablePostActivationDeploymentError(error, deployment) {
|
|
|
706
925
|
}
|
|
707
926
|
|
|
708
927
|
module.exports = {
|
|
928
|
+
assertCandidateBundleFilesCurrent,
|
|
929
|
+
assertCandidateEnvironmentBindingsCurrent,
|
|
709
930
|
bindEnvironmentTarget,
|
|
931
|
+
buildCandidateEnvironmentBindings,
|
|
710
932
|
buildSideEffectPolicyDiff,
|
|
711
933
|
buildCandidateBundle,
|
|
712
934
|
canonicalJson,
|
|
@@ -718,6 +940,8 @@ module.exports = {
|
|
|
718
940
|
normalizeSideEffectPolicyInput,
|
|
719
941
|
normalizeTargetName,
|
|
720
942
|
hasStateResourceMappings,
|
|
943
|
+
inspectCandidateBundleFiles,
|
|
944
|
+
inspectCandidateEnvironmentBindings,
|
|
721
945
|
managedTargetDeploymentFile,
|
|
722
946
|
readCandidate,
|
|
723
947
|
readJsonInput,
|