@actiondock/core 2.19.0 → 2.21.0
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/dist/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +13 -1
- package/dist/errors.js +25 -0
- package/dist/execution/service.js +44 -10
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +20 -1
- package/dist/host/host.js +430 -66
- package/dist/host/types.d.ts +13 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +33 -0
- package/dist/invocation/types.js +11 -1
- package/dist/package/index.d.ts +2 -2
- package/dist/package/runtime.js +48 -15
- package/dist/platform/node.d.ts +6 -0
- package/dist/platform/node.js +59 -16
- package/dist/platform/types.d.ts +59 -1
- package/dist/process/command-executor.d.ts +35 -0
- package/dist/process/command-executor.js +326 -0
- package/dist/process/index.d.ts +1 -0
- package/dist/process/index.js +1 -0
- package/dist/process/process-driver.d.ts +19 -6
- package/dist/process/process-driver.js +223 -54
- package/dist/process/run-executor.js +101 -47
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/dependencies.d.ts +92 -0
- package/dist/project/dependencies.js +543 -0
- package/dist/project/index.d.ts +8 -2
- package/dist/project/index.js +8 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +19 -30
- package/dist/project/manifest.js +10 -0
- package/dist/project/package-manager.d.ts +40 -0
- package/dist/project/package-manager.js +224 -0
- package/dist/project/preparation.d.ts +8 -0
- package/dist/project/preparation.js +130 -0
- package/dist/project/transactions.d.ts +5 -1
- package/dist/project/transactions.js +35 -75
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +27 -5
- package/dist/runtime/context.js +98 -26
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/run-persistence.d.ts +1 -1
- package/dist/runtime/run-persistence.js +4 -3
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/security/config-cipher.d.ts +54 -0
- package/dist/security/config-cipher.js +190 -0
- package/dist/security/config-key.d.ts +35 -0
- package/dist/security/config-key.js +469 -0
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +84 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +4 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +28 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/config-value.d.ts +33 -0
- package/dist/storage/config-value.js +102 -0
- package/dist/storage/file-lock.js +35 -21
- package/dist/storage/index.d.ts +24 -2
- package/dist/storage/index.js +65 -2
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/storage/migrations/index.d.ts +40 -0
- package/dist/storage/migrations/index.js +96 -0
- package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
- package/dist/storage/migrations/v2-to-v3.js +116 -0
- package/dist/storage/sqlite-config.d.ts +62 -3
- package/dist/storage/sqlite-config.js +163 -9
- package/dist/storage/sqlite-driver.d.ts +5 -2
- package/dist/storage/sqlite-driver.js +24 -3
- package/dist/storage/sqlite-schema.d.ts +20 -2
- package/dist/storage/sqlite-schema.js +129 -132
- package/dist/storage/sqlite.d.ts +7 -2
- package/dist/storage/sqlite.js +64 -13
- package/dist/storage/types.d.ts +72 -4
- package/dist/storage/types.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ConfigItemDefinition } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 判定指定配置键名是否为通配模板键。
|
|
4
|
+
*
|
|
5
|
+
* @param key 待检测的配置键名
|
|
6
|
+
*/
|
|
7
|
+
export declare function isConfigTemplate(key: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 断言键名为具体配置键,拒绝空字符串、非字符串以及模板通配键。
|
|
10
|
+
*
|
|
11
|
+
* @param key 待检查的具体配置键名
|
|
12
|
+
*/
|
|
13
|
+
export declare function assertConcreteConfigKey(key: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* 解析配置项命中结果结构。
|
|
16
|
+
*/
|
|
17
|
+
export interface ResolvedConfigDeclaration {
|
|
18
|
+
/** 命中声明所登记的原始键名(具体键或模板键) */
|
|
19
|
+
key: string;
|
|
20
|
+
/** 声明的配置定义元数据 */
|
|
21
|
+
item: ConfigItemDefinition;
|
|
22
|
+
/** 若通过模板匹配命中,则返回命中的模板表达式 */
|
|
23
|
+
template?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析并匹配指定配置键在声明字典中的定义项。
|
|
27
|
+
*
|
|
28
|
+
* 匹配优先级规则:
|
|
29
|
+
* - 具体声明键严格优先于任何模板通配键;
|
|
30
|
+
* - 模板只允许包含单个星号(*),匹配一个或多个合法标识字符([A-Za-z0-9_.-]);
|
|
31
|
+
* - 多个模板同时命中时,优先选取前缀最长者,前缀长度相同时选取后缀最长者,杜绝对象枚举遍历顺序影响。
|
|
32
|
+
*
|
|
33
|
+
* @param config 声明的配置定义字典
|
|
34
|
+
* @param key 实际访问或配置的具体键名
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveConfigDeclaration(config: Record<string, ConfigItemDefinition> | undefined, key: string): ResolvedConfigDeclaration | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* 校验配置声明集合中的通配模板与受限 Schema 规范。
|
|
39
|
+
* 针对历史无模板、无 Schema 项保持完全兼容。
|
|
40
|
+
*
|
|
41
|
+
* @param config 待校验的配置声明定义
|
|
42
|
+
* @returns 错误信息字符串数组,无错误返回空数组
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateConfigDeclarations(config: unknown): string[];
|
|
45
|
+
/**
|
|
46
|
+
* 断言配置声明集合完全合法。
|
|
47
|
+
* 若存在非法模板或不合规 Schema 抛出 MANIFEST_INVALID 异常。
|
|
48
|
+
*
|
|
49
|
+
* @param config 待校验的配置声明定义
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertValidConfigDeclarations(config: unknown): void;
|
|
52
|
+
/**
|
|
53
|
+
* 校验指定配置数据值是否符合绑定的 Schema 契约。
|
|
54
|
+
* 仅在声明了 schema 且数据值不为 undefined 时执行校验。
|
|
55
|
+
* 错误信息输出键名与失败路径原因,严禁包含实际数据值。
|
|
56
|
+
*
|
|
57
|
+
* @param key 配置项键名
|
|
58
|
+
* @param value 待校验的具体数据值
|
|
59
|
+
* @param item 配置项元数据定义
|
|
60
|
+
*/
|
|
61
|
+
export declare function validateConfigValue(key: string, value: unknown, item?: ConfigItemDefinition): void;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { ActionDockError, CONFIG_VALUE_INVALID, INVALID_ARGUMENT, MANIFEST_INVALID, } from "../errors.js";
|
|
2
|
+
import { validateSchema, validateSchemaOnly } from "../schema/validator.js";
|
|
3
|
+
/**
|
|
4
|
+
* 判定指定配置键名是否为通配模板键。
|
|
5
|
+
*
|
|
6
|
+
* @param key 待检测的配置键名
|
|
7
|
+
*/
|
|
8
|
+
export function isConfigTemplate(key) {
|
|
9
|
+
return typeof key === "string" && key.includes("*");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 断言键名为具体配置键,拒绝空字符串、非字符串以及模板通配键。
|
|
13
|
+
*
|
|
14
|
+
* @param key 待检查的具体配置键名
|
|
15
|
+
*/
|
|
16
|
+
export function assertConcreteConfigKey(key) {
|
|
17
|
+
if (typeof key !== "string" || key.length === 0 || key.includes("*")) {
|
|
18
|
+
throw new ActionDockError(INVALID_ARGUMENT, `Concrete configuration key expected, received invalid key: '${key}'`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function escapeRegex(str) {
|
|
22
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 解析并匹配指定配置键在声明字典中的定义项。
|
|
26
|
+
*
|
|
27
|
+
* 匹配优先级规则:
|
|
28
|
+
* - 具体声明键严格优先于任何模板通配键;
|
|
29
|
+
* - 模板只允许包含单个星号(*),匹配一个或多个合法标识字符([A-Za-z0-9_.-]);
|
|
30
|
+
* - 多个模板同时命中时,优先选取前缀最长者,前缀长度相同时选取后缀最长者,杜绝对象枚举遍历顺序影响。
|
|
31
|
+
*
|
|
32
|
+
* @param config 声明的配置定义字典
|
|
33
|
+
* @param key 实际访问或配置的具体键名
|
|
34
|
+
*/
|
|
35
|
+
export function resolveConfigDeclaration(config, key) {
|
|
36
|
+
if (!config || typeof config !== "object" || typeof key !== "string") {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// 具体键严格优先
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(config, key)) {
|
|
41
|
+
return {
|
|
42
|
+
key,
|
|
43
|
+
item: config[key],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const candidates = [];
|
|
47
|
+
for (const [declaredKey, item] of Object.entries(config)) {
|
|
48
|
+
if (!isConfigTemplate(declaredKey)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const parts = declaredKey.split("*");
|
|
52
|
+
if (parts.length !== 2) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const [prefix, suffix] = parts;
|
|
56
|
+
const pattern = new RegExp(`^${escapeRegex(prefix)}([A-Za-z0-9_.-]+)${escapeRegex(suffix)}$`);
|
|
57
|
+
if (pattern.exec(key)?.[0] === key) {
|
|
58
|
+
candidates.push({
|
|
59
|
+
templateKey: declaredKey,
|
|
60
|
+
item,
|
|
61
|
+
prefixLength: prefix.length,
|
|
62
|
+
suffixLength: suffix.length,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (candidates.length === 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
// 判定最长前缀再最长后缀,稳定按字典序平局兜底
|
|
70
|
+
candidates.sort((a, b) => {
|
|
71
|
+
if (b.prefixLength !== a.prefixLength) {
|
|
72
|
+
return b.prefixLength - a.prefixLength;
|
|
73
|
+
}
|
|
74
|
+
if (b.suffixLength !== a.suffixLength) {
|
|
75
|
+
return b.suffixLength - a.suffixLength;
|
|
76
|
+
}
|
|
77
|
+
return a.templateKey.localeCompare(b.templateKey);
|
|
78
|
+
});
|
|
79
|
+
const best = candidates[0];
|
|
80
|
+
return {
|
|
81
|
+
key: best.templateKey,
|
|
82
|
+
item: best.item,
|
|
83
|
+
template: best.templateKey,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** 受限 JSON Schema 允许的白名单关键字集合 */
|
|
87
|
+
const ALLOWED_SCHEMA_KEYWORDS = new Set([
|
|
88
|
+
"type",
|
|
89
|
+
"properties",
|
|
90
|
+
"required",
|
|
91
|
+
"additionalProperties",
|
|
92
|
+
"items",
|
|
93
|
+
"enum",
|
|
94
|
+
"minLength",
|
|
95
|
+
"maxLength",
|
|
96
|
+
"minimum",
|
|
97
|
+
"maximum",
|
|
98
|
+
"secret",
|
|
99
|
+
]);
|
|
100
|
+
/** 模板键中严禁声明的保留字段 */
|
|
101
|
+
const FORBIDDEN_TEMPLATE_FIELDS = ["required", "default", "env"];
|
|
102
|
+
function validateSchemaNode(schemaNode, path, errors) {
|
|
103
|
+
if (typeof schemaNode !== "object" || schemaNode === null || Array.isArray(schemaNode)) {
|
|
104
|
+
errors.push(`Schema at '${path}' must be an object`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const k of Object.keys(schemaNode)) {
|
|
108
|
+
if (!ALLOWED_SCHEMA_KEYWORDS.has(k)) {
|
|
109
|
+
errors.push(`Schema at '${path}' contains unsupported keyword '${k}'`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const node = schemaNode;
|
|
113
|
+
if (node.type !== undefined) {
|
|
114
|
+
if (typeof node.type !== "string" ||
|
|
115
|
+
!["object", "array", "string", "integer", "number", "boolean"].includes(node.type)) {
|
|
116
|
+
errors.push(`Schema at '${path}' property 'type' must be one of "object", "array", "string", "integer", "number", "boolean"`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (node.properties !== undefined) {
|
|
120
|
+
if (typeof node.properties !== "object" ||
|
|
121
|
+
node.properties === null ||
|
|
122
|
+
Array.isArray(node.properties)) {
|
|
123
|
+
errors.push(`Schema at '${path}' property 'properties' must be an object`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const [propName, child] of Object.entries(node.properties)) {
|
|
127
|
+
validateSchemaNode(child, `${path}/properties/${propName}`, errors);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (node.required !== undefined) {
|
|
132
|
+
if (!Array.isArray(node.required) ||
|
|
133
|
+
node.required.some((r) => typeof r !== "string")) {
|
|
134
|
+
errors.push(`Schema at '${path}' property 'required' must be an array of strings`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (node.additionalProperties !== undefined) {
|
|
138
|
+
if (typeof node.additionalProperties === "boolean") {
|
|
139
|
+
// 允许布尔配置
|
|
140
|
+
}
|
|
141
|
+
else if (typeof node.additionalProperties === "object" &&
|
|
142
|
+
node.additionalProperties !== null &&
|
|
143
|
+
!Array.isArray(node.additionalProperties)) {
|
|
144
|
+
validateSchemaNode(node.additionalProperties, `${path}/additionalProperties`, errors);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
errors.push(`Schema at '${path}' property 'additionalProperties' must be a boolean or a schema object`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (node.items !== undefined) {
|
|
151
|
+
if (typeof node.items === "object" &&
|
|
152
|
+
node.items !== null &&
|
|
153
|
+
!Array.isArray(node.items)) {
|
|
154
|
+
validateSchemaNode(node.items, `${path}/items`, errors);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
errors.push(`Schema at '${path}' property 'items' must be a schema object`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (node.enum !== undefined) {
|
|
161
|
+
if (!Array.isArray(node.enum)) {
|
|
162
|
+
errors.push(`Schema at '${path}' property 'enum' must be an array`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (node.minLength !== undefined) {
|
|
166
|
+
if (typeof node.minLength !== "number" ||
|
|
167
|
+
!Number.isInteger(node.minLength) ||
|
|
168
|
+
node.minLength < 0) {
|
|
169
|
+
errors.push(`Schema at '${path}' property 'minLength' must be a non-negative integer`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (node.maxLength !== undefined) {
|
|
173
|
+
if (typeof node.maxLength !== "number" ||
|
|
174
|
+
!Number.isInteger(node.maxLength) ||
|
|
175
|
+
node.maxLength < 0) {
|
|
176
|
+
errors.push(`Schema at '${path}' property 'maxLength' must be a non-negative integer`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (node.minimum !== undefined) {
|
|
180
|
+
if (typeof node.minimum !== "number" || !Number.isFinite(node.minimum)) {
|
|
181
|
+
errors.push(`Schema at '${path}' property 'minimum' must be a number`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (node.maximum !== undefined) {
|
|
185
|
+
if (typeof node.maximum !== "number" || !Number.isFinite(node.maximum)) {
|
|
186
|
+
errors.push(`Schema at '${path}' property 'maximum' must be a number`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (node.secret !== undefined) {
|
|
190
|
+
if (typeof node.secret !== "boolean") {
|
|
191
|
+
errors.push(`Schema at '${path}' property 'secret' must be a boolean`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 校验配置声明集合中的通配模板与受限 Schema 规范。
|
|
197
|
+
* 针对历史无模板、无 Schema 项保持完全兼容。
|
|
198
|
+
*
|
|
199
|
+
* @param config 待校验的配置声明定义
|
|
200
|
+
* @returns 错误信息字符串数组,无错误返回空数组
|
|
201
|
+
*/
|
|
202
|
+
export function validateConfigDeclarations(config) {
|
|
203
|
+
if (config === undefined || config === null) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
if (typeof config !== "object" || Array.isArray(config)) {
|
|
207
|
+
return ["Config declaration must be an object"];
|
|
208
|
+
}
|
|
209
|
+
const errors = [];
|
|
210
|
+
for (const [key, item] of Object.entries(config)) {
|
|
211
|
+
const isTemplate = isConfigTemplate(key);
|
|
212
|
+
if (isTemplate) {
|
|
213
|
+
const starCount = (key.match(/\*/g) || []).length;
|
|
214
|
+
if (starCount !== 1) {
|
|
215
|
+
errors.push(`Config template key '${key}' must contain exactly one '*' wildcard`);
|
|
216
|
+
}
|
|
217
|
+
if (item && typeof item === "object") {
|
|
218
|
+
for (const forbidden of FORBIDDEN_TEMPLATE_FIELDS) {
|
|
219
|
+
if (forbidden in item) {
|
|
220
|
+
errors.push(`Config template '${key}' must not declare '${forbidden}' property`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
226
|
+
if (isTemplate)
|
|
227
|
+
errors.push(`Config template '${key}' must declare an object`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const itemDef = item;
|
|
231
|
+
if ((isTemplate || "schema" in itemDef) && itemDef.secret !== undefined && typeof itemDef.secret !== "boolean") {
|
|
232
|
+
errors.push(`Config item '${key}' property 'secret' must be a boolean`);
|
|
233
|
+
}
|
|
234
|
+
if ("schema" in itemDef && itemDef.schema !== undefined) {
|
|
235
|
+
if (typeof itemDef.schema !== "object" ||
|
|
236
|
+
itemDef.schema === null ||
|
|
237
|
+
Array.isArray(itemDef.schema)) {
|
|
238
|
+
errors.push(`Config item '${key}' schema must be an object`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
// 递归白名单与属性类型校验
|
|
242
|
+
validateSchemaNode(itemDef.schema, `config['${key}'].schema`, errors);
|
|
243
|
+
// 外层 type 与根 schema.type 一致性检查(外层 number 与模式 integer 允许兼容)
|
|
244
|
+
if (itemDef.type !== undefined && itemDef.schema.type !== undefined) {
|
|
245
|
+
if (itemDef.type === "number") {
|
|
246
|
+
if (itemDef.schema.type !== "number" && itemDef.schema.type !== "integer") {
|
|
247
|
+
errors.push(`Config item '${key}' outer type 'number' is incompatible with schema type '${itemDef.schema.type}'`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (itemDef.type !== itemDef.schema.type) {
|
|
251
|
+
errors.push(`Config item '${key}' outer type '${itemDef.type}' is incompatible with schema type '${itemDef.schema.type}'`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 利用现有 Ajv 进行 Schema 编译合法性校验
|
|
255
|
+
const ajvCheck = validateSchemaOnly(itemDef.schema, {});
|
|
256
|
+
if (!ajvCheck.valid && ajvCheck.errors) {
|
|
257
|
+
for (const err of ajvCheck.errors) {
|
|
258
|
+
if (err.startsWith("Schema compilation error:")) {
|
|
259
|
+
errors.push(`Config item '${key}' schema failed compilation: ${err}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return errors;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* 断言配置声明集合完全合法。
|
|
269
|
+
* 若存在非法模板或不合规 Schema 抛出 MANIFEST_INVALID 异常。
|
|
270
|
+
*
|
|
271
|
+
* @param config 待校验的配置声明定义
|
|
272
|
+
*/
|
|
273
|
+
export function assertValidConfigDeclarations(config) {
|
|
274
|
+
const errors = validateConfigDeclarations(config);
|
|
275
|
+
if (errors.length > 0) {
|
|
276
|
+
throw new ActionDockError(MANIFEST_INVALID, `Invalid configuration declarations: ${errors.join("; ")}`, { errors });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 校验指定配置数据值是否符合绑定的 Schema 契约。
|
|
281
|
+
* 仅在声明了 schema 且数据值不为 undefined 时执行校验。
|
|
282
|
+
* 错误信息输出键名与失败路径原因,严禁包含实际数据值。
|
|
283
|
+
*
|
|
284
|
+
* @param key 配置项键名
|
|
285
|
+
* @param value 待校验的具体数据值
|
|
286
|
+
* @param item 配置项元数据定义
|
|
287
|
+
*/
|
|
288
|
+
export function validateConfigValue(key, value, item) {
|
|
289
|
+
if (!item?.schema || value === undefined) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const result = validateSchema(item.schema, value);
|
|
293
|
+
if (!result.valid) {
|
|
294
|
+
const reasons = (result.errors || []).join("; ");
|
|
295
|
+
throw new ActionDockError(CONFIG_VALUE_INVALID, `Configuration value for '${key}' failed schema validation: ${reasons}`, { key, errors: result.errors });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProcessDriver } from "../process/driver.js";
|
|
2
|
+
export interface EnsureProjectDependenciesOptions {
|
|
3
|
+
/** 是否允许自动安装(默认依据 process.env.ACTIONDOCK_AUTO_INSTALL !== "false") */
|
|
4
|
+
install?: boolean;
|
|
5
|
+
/** 取消信号 */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/** 进度输出回调,默认输出至 process.stderr */
|
|
8
|
+
onProgress?: (message: string) => void;
|
|
9
|
+
/** 自定义安装超时(毫秒),默认 5 分钟 (300,000 ms) */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** 可选注入的底层进程驱动(用于确定性测试) */
|
|
12
|
+
processDriver?: ProcessDriver;
|
|
13
|
+
}
|
|
14
|
+
export interface EnsureProjectDependenciesResult {
|
|
15
|
+
/** 是否触发并完成了安装 */
|
|
16
|
+
installed: boolean;
|
|
17
|
+
/** 实际安装根目录 */
|
|
18
|
+
installRoot?: string;
|
|
19
|
+
/** 使用的包管理器(若进行了安装) */
|
|
20
|
+
packageManager?: "npm" | "bun";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 解析并确定目标工程的实际安装根目录。
|
|
24
|
+
*
|
|
25
|
+
* 归属规则:
|
|
26
|
+
* - 位于 node_modules 目录内的包(包括经符号链接指向物理 node_modules 的情形),直接跳过(不在其内部再执行安装);
|
|
27
|
+
* - 无 package.json 的工程,直接跳过(保留无外部依赖合法工程的运行路径);
|
|
28
|
+
* - 工作区子包:校验上级 package.json.workspaces 归属,确属工作区子包时使用工作区根目录;
|
|
29
|
+
* - 仅接受实际 workspace 声明归属,不因祖先存在锁文件误认工作区根;
|
|
30
|
+
* - 校验 package.json 及 workspaces 声明类型,严禁吞没 JSON 解析与 I/O 异常;
|
|
31
|
+
* - 独立包:使用自身 package.json 所在目录作为安装根。
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveInstallRoot(packageRoot: string): {
|
|
34
|
+
installRoot?: string;
|
|
35
|
+
isWorkspace: boolean;
|
|
36
|
+
skip: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
workspacePatterns?: string[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* 检查当前包声明的必需直接生产依赖是否已在 node_modules 中物化。
|
|
42
|
+
*
|
|
43
|
+
* 判定设计契约:
|
|
44
|
+
* - 仅检查 package.json 中的 dependencies 生产依赖;
|
|
45
|
+
* - 校验 dependencies 与 optionalDependencies 的记录对象结构,杜绝数组或无效数据;
|
|
46
|
+
* - 使用 Object.hasOwn 判定可选依赖排除,防止原型属性污染及误判;
|
|
47
|
+
* - optionalDependencies 缺失不视为必须安装;同名条目在 optionalDependencies 中声明时覆盖 dependencies;
|
|
48
|
+
* - 使用 node:module.findPackageJSON 仅探测依赖包元数据是否存在,不代表完整依赖加载健康;
|
|
49
|
+
* - 仅模块未找到(ERR_MODULE_NOT_FOUND)归类为 missing,读取权限与损坏配置等错误必须向外显式抛出;
|
|
50
|
+
* - 无需检查全部传递依赖。
|
|
51
|
+
*/
|
|
52
|
+
export declare function checkDependenciesMaterialized(packageRoot: string): {
|
|
53
|
+
ready: boolean;
|
|
54
|
+
missing: string[];
|
|
55
|
+
requiredCount: number;
|
|
56
|
+
};
|
|
57
|
+
export interface PackageManagerPlan {
|
|
58
|
+
/** 决策选定的包管理器 */
|
|
59
|
+
manager: "npm" | "bun";
|
|
60
|
+
/** 安装根是否存在该管理器对应的锁文件 */
|
|
61
|
+
hasLock: boolean;
|
|
62
|
+
/** 该管理器在当前锁状态下应执行的安装参数 */
|
|
63
|
+
args: string[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 解析安装根的锁文件、packageManager 声明与环境变量,产出确定性的包管理器安装决策。
|
|
67
|
+
*
|
|
68
|
+
* 决策契约(优先级从高到低):
|
|
69
|
+
* - 环境变量 ACTIONDOCK_INSTALLER 显式指定 > package.json packageManager 声明 > 锁文件归属 > 默认 npm;
|
|
70
|
+
* - 多锁文件冲突、声明与环境变量互相冲突、声明或环境变量为不支持的工具时,一律显式拒绝;
|
|
71
|
+
* - 检测到 pnpm 或 yarn 工程时引导手动准备,不自动安装;
|
|
72
|
+
* - 有锁走冻结安装(npm ci / bun --frozen-lockfile),无锁走普通生产安装。
|
|
73
|
+
*
|
|
74
|
+
* @param installRoot 安装根目录
|
|
75
|
+
* @param env 环境变量视图(默认 process.env,测试可注入确定性环境)
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolvePackageManagerPlan(installRoot: string, env?: Record<string, string | undefined>): PackageManagerPlan;
|
|
78
|
+
/**
|
|
79
|
+
* 确保项目所需生产依赖处于就绪状态。
|
|
80
|
+
*
|
|
81
|
+
* 核心设计原则:
|
|
82
|
+
* - 仅在所需必需生产依赖未物化、且安装根及工作区未探测到已有依赖目录时自动触发首次依赖安装;
|
|
83
|
+
* - 已有 node_modules 但依赖缺失或损坏时不自动重建,抛出 DEPENDENCIES_NOT_READY 并提供修复指引;
|
|
84
|
+
* - 明确禁止安装时(--no-install 或 ACTIONDOCK_AUTO_INSTALL=false),抛出 DEPENDENCIES_NOT_READY;
|
|
85
|
+
* - 存在未恢复的悬空事务时,抛出 PROJECT_RECOVERY_REQUIRED;
|
|
86
|
+
* - 取消信号生效时抛出 ACTION_CANCELLED;
|
|
87
|
+
* - 受工程排他锁保护,并发冲突时抛出 PROJECT_BUSY。
|
|
88
|
+
*
|
|
89
|
+
* @param packageRoot 目标包根目录
|
|
90
|
+
* @param options 执行选项
|
|
91
|
+
*/
|
|
92
|
+
export declare function ensureProjectDependencies(requestedPackageRoot: string, options?: EnsureProjectDependenciesOptions): Promise<EnsureProjectDependenciesResult>;
|