bdy 1.24.8 → 1.25.0-dev
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/distTs/detect-rules.json +351 -0
- package/distTs/package.json +7 -2
- package/distTs/src/cliIndex.js +2 -0
- package/distTs/src/command/yaml/actions/detect.js +268 -0
- package/distTs/src/command/yaml/actions/info.js +79 -0
- package/distTs/src/command/yaml/actions/list.js +96 -0
- package/distTs/src/command/yaml/actions/schema.js +104 -0
- package/distTs/src/command/yaml/actions.js +16 -0
- package/distTs/src/command/yaml/cache.js +106 -0
- package/distTs/src/command/yaml/pipeline.js +53 -0
- package/distTs/src/command/yaml/render.js +111 -0
- package/distTs/src/command/yaml/schemaUtils.js +164 -0
- package/distTs/src/command/yaml/validate.js +300 -0
- package/distTs/src/command/yaml.js +20 -0
- package/distTs/src/diskCache.js +200 -0
- package/distTs/src/openapi.js +10 -81
- package/distTs/src/texts.js +66 -1
- package/distTs/src/utils.js +18 -13
- package/package.json +7 -2
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureYamlSchema = ensureYamlSchema;
|
|
7
|
+
exports.loadRawSchema = loadRawSchema;
|
|
8
|
+
exports.getDefValidator = getDefValidator;
|
|
9
|
+
exports.validateAgainstDef = validateAgainstDef;
|
|
10
|
+
exports.validateAgainstSchema = validateAgainstSchema;
|
|
11
|
+
exports.getAllDefs = getAllDefs;
|
|
12
|
+
exports.getSharedDefs = getSharedDefs;
|
|
13
|
+
exports.resolveRefs = resolveRefs;
|
|
14
|
+
const utils_1 = require("../../utils");
|
|
15
|
+
const input_1 = __importDefault(require("../../input"));
|
|
16
|
+
const diskCache_1 = require("../../diskCache");
|
|
17
|
+
const texts_1 = require("../../texts");
|
|
18
|
+
function schemaUrl() {
|
|
19
|
+
const env = (0, utils_1.getApiEnv)(input_1.default.restApiTokenClient(true).baseUrl);
|
|
20
|
+
return `https://es.buddy.works/yaml-schema/${env}/yaml-schema.json`;
|
|
21
|
+
}
|
|
22
|
+
async function ensureYamlSchema() {
|
|
23
|
+
const url = schemaUrl();
|
|
24
|
+
const paths = (0, diskCache_1.cachePaths)('yaml-schema', url);
|
|
25
|
+
if ((0, diskCache_1.isHardFresh)((0, diskCache_1.readMeta)(paths.meta)))
|
|
26
|
+
return;
|
|
27
|
+
if ((await (0, diskCache_1.refresh)(url, paths)) === 'none') {
|
|
28
|
+
throw new Error((0, texts_1.ERR_YAML_SCHEMA_FETCH_FAILED)(url));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function readCachedSchema() {
|
|
32
|
+
const path = (0, diskCache_1.cachePaths)('yaml-schema', schemaUrl()).raw;
|
|
33
|
+
const raw = (0, diskCache_1.readCached)(path);
|
|
34
|
+
if (!raw)
|
|
35
|
+
throw new Error((0, texts_1.ERR_YAML_SCHEMA_READ_FAILED)(path));
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
let cachedRaw = null;
|
|
39
|
+
let ajvInstance = null;
|
|
40
|
+
const validatorCache = new Map();
|
|
41
|
+
function loadRawSchema() {
|
|
42
|
+
if (cachedRaw)
|
|
43
|
+
return cachedRaw;
|
|
44
|
+
cachedRaw = readCachedSchema();
|
|
45
|
+
return cachedRaw;
|
|
46
|
+
}
|
|
47
|
+
// Read off the schema, not listed: AJV strict mode rejects any x- keyword it was
|
|
48
|
+
// not told about, and the backend keeps adding them.
|
|
49
|
+
function vendorKeywords(schema) {
|
|
50
|
+
const found = new Set();
|
|
51
|
+
const walk = (node) => {
|
|
52
|
+
if (!node || typeof node !== 'object')
|
|
53
|
+
return;
|
|
54
|
+
if (Array.isArray(node)) {
|
|
55
|
+
node.forEach(walk);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const [key, value] of Object.entries(node)) {
|
|
59
|
+
if (key.startsWith('x-'))
|
|
60
|
+
found.add(key);
|
|
61
|
+
walk(value);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
walk(schema);
|
|
65
|
+
return [...found];
|
|
66
|
+
}
|
|
67
|
+
function getAjv() {
|
|
68
|
+
if (ajvInstance)
|
|
69
|
+
return ajvInstance;
|
|
70
|
+
const Ajv = require('ajv/dist/2020').default;
|
|
71
|
+
const addFormats = require('ajv-formats').default;
|
|
72
|
+
const schema = loadRawSchema();
|
|
73
|
+
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
|
|
74
|
+
addFormats(ajv);
|
|
75
|
+
for (const kw of vendorKeywords(schema))
|
|
76
|
+
ajv.addKeyword(kw);
|
|
77
|
+
ajv.addKeyword('discriminator');
|
|
78
|
+
ajv.addSchema(schema);
|
|
79
|
+
ajvInstance = ajv;
|
|
80
|
+
return ajv;
|
|
81
|
+
}
|
|
82
|
+
function getDefValidator(defName) {
|
|
83
|
+
if (validatorCache.has(defName))
|
|
84
|
+
return validatorCache.get(defName);
|
|
85
|
+
const ajv = getAjv();
|
|
86
|
+
const raw = loadRawSchema();
|
|
87
|
+
const validate = ajv.compile({ $ref: `${raw.$id}#/$defs/${defName}` });
|
|
88
|
+
validatorCache.set(defName, validate);
|
|
89
|
+
return validate;
|
|
90
|
+
}
|
|
91
|
+
function formatErrors(errors) {
|
|
92
|
+
if (!errors)
|
|
93
|
+
return [];
|
|
94
|
+
return errors.map((err) => {
|
|
95
|
+
let errPath = err.instancePath.replace(/^\//, '').replace(/\//g, '.');
|
|
96
|
+
if (err.keyword === 'required' && err.params?.missingProperty) {
|
|
97
|
+
errPath = errPath ? `${errPath}.${err.params.missingProperty}` : err.params.missingProperty;
|
|
98
|
+
}
|
|
99
|
+
return { path: errPath, message: err.message || 'unknown error' };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function validateAgainstDef(defName, data) {
|
|
103
|
+
const validate = getDefValidator(defName);
|
|
104
|
+
const valid = validate(data);
|
|
105
|
+
return { valid: !!valid, errors: formatErrors(validate.errors) };
|
|
106
|
+
}
|
|
107
|
+
function validateAgainstSchema(schema, data) {
|
|
108
|
+
const ajv = getAjv();
|
|
109
|
+
const validate = ajv.compile(schema);
|
|
110
|
+
const valid = validate(data);
|
|
111
|
+
return { valid: !!valid, errors: formatErrors(validate.errors) };
|
|
112
|
+
}
|
|
113
|
+
function getAllDefs() {
|
|
114
|
+
const raw = loadRawSchema();
|
|
115
|
+
return raw.$defs || {};
|
|
116
|
+
}
|
|
117
|
+
function getSharedDefs() {
|
|
118
|
+
const defs = getAllDefs();
|
|
119
|
+
const shared = {};
|
|
120
|
+
for (const [k, v] of Object.entries(defs)) {
|
|
121
|
+
if (!k.endsWith('ActionYaml')) {
|
|
122
|
+
shared[k] = v;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return shared;
|
|
126
|
+
}
|
|
127
|
+
function resolveRefs(properties, sharedDefs, depth = 0) {
|
|
128
|
+
if (depth > 5)
|
|
129
|
+
return properties;
|
|
130
|
+
const resolved = {};
|
|
131
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
132
|
+
if (prop.$ref) {
|
|
133
|
+
const refName = prop.$ref.replace('#/$defs/', '');
|
|
134
|
+
const refDef = sharedDefs[refName];
|
|
135
|
+
if (refDef) {
|
|
136
|
+
resolved[key] = { ...refDef, description: prop.description || refDef.description };
|
|
137
|
+
if (refDef.properties) {
|
|
138
|
+
resolved[key].properties = resolveRefs(refDef.properties, sharedDefs, depth + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
resolved[key] = prop;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (prop.type === 'array' && prop.items?.$ref) {
|
|
146
|
+
const refName = prop.items.$ref.replace('#/$defs/', '');
|
|
147
|
+
const refDef = sharedDefs[refName];
|
|
148
|
+
if (refDef) {
|
|
149
|
+
const resolvedItems = { ...refDef };
|
|
150
|
+
if (refDef.properties) {
|
|
151
|
+
resolvedItems.properties = resolveRefs(refDef.properties, sharedDefs, depth + 1);
|
|
152
|
+
}
|
|
153
|
+
resolved[key] = { ...prop, items: resolvedItems };
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
resolved[key] = prop;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
resolved[key] = prop;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return resolved;
|
|
164
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const utils_1 = require("../../utils");
|
|
9
|
+
const output_1 = __importDefault(require("../../output"));
|
|
10
|
+
const texts_1 = require("../../texts");
|
|
11
|
+
const schemaUtils_1 = require("./schemaUtils");
|
|
12
|
+
// Valid top-level keys the schema's PipelineYaml.properties omits (everything
|
|
13
|
+
// else is derived from the schema, so this only covers genuine gaps).
|
|
14
|
+
const SCHEMA_OMITTED_TOP_LEVEL_KEYS = ['git_config_ref'];
|
|
15
|
+
const LABEL_KEYS = ['action', 'target', 'name', 'key', 'title', 'trigger_condition'];
|
|
16
|
+
function stripRef(ref) {
|
|
17
|
+
return ref.replace('#/$defs/', '');
|
|
18
|
+
}
|
|
19
|
+
function prettifyKey(key) {
|
|
20
|
+
return key
|
|
21
|
+
.split('_')
|
|
22
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
23
|
+
.join(' ');
|
|
24
|
+
}
|
|
25
|
+
function outputErrors(result, indent) {
|
|
26
|
+
for (const err of result.errors) {
|
|
27
|
+
output_1.default.muted(`${indent}${err.path}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function valueType(value) {
|
|
31
|
+
if (value === null)
|
|
32
|
+
return 'null';
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return 'array';
|
|
35
|
+
return typeof value;
|
|
36
|
+
}
|
|
37
|
+
function reportWrongContainer(header, expected, value) {
|
|
38
|
+
output_1.default.error(header);
|
|
39
|
+
output_1.default.muted(` ${(0, texts_1.ERR_YAML_MUST_BE)(expected, valueType(value))}`);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
// Array whose items reference a def — directly, or via a single $ref branch in
|
|
43
|
+
// items.oneOf (how `targets` mixes a string ref with an object form).
|
|
44
|
+
function itemRef(prop) {
|
|
45
|
+
if (!prop || prop.type !== 'array' || !prop.items)
|
|
46
|
+
return null;
|
|
47
|
+
if (prop.items.$ref)
|
|
48
|
+
return { defName: stripRef(prop.items.$ref), scalar: null };
|
|
49
|
+
if (Array.isArray(prop.items.oneOf)) {
|
|
50
|
+
const refs = prop.items.oneOf.filter((b) => b && b.$ref);
|
|
51
|
+
const scalars = prop.items.oneOf.filter((b) => b && !b.$ref);
|
|
52
|
+
if (refs.length === 1) {
|
|
53
|
+
return {
|
|
54
|
+
defName: stripRef(refs[0].$ref),
|
|
55
|
+
scalar: scalars.length === 1 ? scalars[0] : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function classifySections(props) {
|
|
62
|
+
const itemSections = [];
|
|
63
|
+
const objectSections = [];
|
|
64
|
+
const settingsProps = {};
|
|
65
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
66
|
+
const ref = itemRef(prop);
|
|
67
|
+
if (ref) {
|
|
68
|
+
itemSections.push({ key, ...ref });
|
|
69
|
+
}
|
|
70
|
+
else if (prop && prop.$ref) {
|
|
71
|
+
objectSections.push({ key, defName: stripRef(prop.$ref), scalar: null });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
settingsProps[key] = prop;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { itemSections, objectSections, settingsProps };
|
|
78
|
+
}
|
|
79
|
+
function discriminatorProp(def) {
|
|
80
|
+
return def && def.oneOf && def.discriminator ? def.discriminator.propertyName : null;
|
|
81
|
+
}
|
|
82
|
+
// Resolve a discriminated def to its concrete branch by the item's `type`, so
|
|
83
|
+
// AJV always validates against a single def (never a oneOf). Plain defs pass through.
|
|
84
|
+
function resolveDef(defs, refName, value) {
|
|
85
|
+
const def = defs[refName];
|
|
86
|
+
const prop = discriminatorProp(def);
|
|
87
|
+
if (!prop)
|
|
88
|
+
return { defName: refName };
|
|
89
|
+
const dv = value?.[prop];
|
|
90
|
+
if (dv === undefined || dv === null)
|
|
91
|
+
return { missingType: prop };
|
|
92
|
+
for (const branch of def.oneOf) {
|
|
93
|
+
const branchName = branch && branch.$ref ? stripRef(branch.$ref) : null;
|
|
94
|
+
if (!branchName)
|
|
95
|
+
continue;
|
|
96
|
+
if (defs[branchName]?.properties?.[prop]?.const === dv)
|
|
97
|
+
return { defName: branchName };
|
|
98
|
+
}
|
|
99
|
+
return { unknownType: String(dv), prop };
|
|
100
|
+
}
|
|
101
|
+
function itemLabel(value, index, discProp) {
|
|
102
|
+
let name;
|
|
103
|
+
for (const k of LABEL_KEYS) {
|
|
104
|
+
if (value && value[k]) {
|
|
105
|
+
name = String(value[k]);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!name && !discProp && value && value.type)
|
|
110
|
+
name = String(value.type);
|
|
111
|
+
if (!name)
|
|
112
|
+
name = `#${index + 1}`;
|
|
113
|
+
return discProp && value && value[discProp] ? `${name} (${value[discProp]})` : name;
|
|
114
|
+
}
|
|
115
|
+
function validateScalarItem(item, section) {
|
|
116
|
+
// Null errors even where the section takes a string: coerceTypes cannot fix a root scalar.
|
|
117
|
+
if (item === null || !section.scalar) {
|
|
118
|
+
output_1.default.error(` ${String(item)}: ${(0, texts_1.ERR_YAML_MUST_BE)('object', valueType(item))}`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
const result = (0, schemaUtils_1.validateAgainstSchema)({ ...section.scalar, $defs: (0, schemaUtils_1.loadRawSchema)().$defs }, item);
|
|
122
|
+
if (result.valid) {
|
|
123
|
+
output_1.default.green(` ${String(item)}`);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
output_1.default.error(` ${String(item)}`);
|
|
127
|
+
outputErrors(result, ' ');
|
|
128
|
+
return result.errors.length;
|
|
129
|
+
}
|
|
130
|
+
function validateItemSection(defs, items, section) {
|
|
131
|
+
const refName = section.defName;
|
|
132
|
+
const discProp = discriminatorProp(defs[refName]);
|
|
133
|
+
let errors = 0;
|
|
134
|
+
for (let i = 0; i < items.length; i++) {
|
|
135
|
+
const item = items[i];
|
|
136
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item)) {
|
|
137
|
+
errors += validateScalarItem(item, section);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const label = itemLabel(item, i, discProp);
|
|
141
|
+
const resolved = resolveDef(defs, refName, item);
|
|
142
|
+
if ('missingType' in resolved) {
|
|
143
|
+
output_1.default.error(` ${label}: ${(0, texts_1.ERR_YAML_ITEM_MISSING)(resolved.missingType)}`);
|
|
144
|
+
errors++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if ('unknownType' in resolved) {
|
|
148
|
+
output_1.default.error(` ${label}: ${(0, texts_1.ERR_YAML_ITEM_UNKNOWN)(resolved.prop, resolved.unknownType)}`);
|
|
149
|
+
errors++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const result = (0, schemaUtils_1.validateAgainstDef)(resolved.defName, item);
|
|
153
|
+
if (result.valid) {
|
|
154
|
+
output_1.default.green(` ${label}`);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
errors += result.errors.length;
|
|
158
|
+
output_1.default.error(` ${label}`);
|
|
159
|
+
outputErrors(result, ' ');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return errors;
|
|
163
|
+
}
|
|
164
|
+
function validateObjectSection(defs, label, value, refName) {
|
|
165
|
+
const resolved = resolveDef(defs, refName, value);
|
|
166
|
+
if ('missingType' in resolved) {
|
|
167
|
+
output_1.default.error(label);
|
|
168
|
+
output_1.default.muted(` ${(0, texts_1.ERR_YAML_ITEM_MISSING)(resolved.missingType)}`);
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
if ('unknownType' in resolved) {
|
|
172
|
+
output_1.default.error(label);
|
|
173
|
+
output_1.default.muted(` ${(0, texts_1.ERR_YAML_ITEM_UNKNOWN)(resolved.prop, resolved.unknownType)}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const result = (0, schemaUtils_1.validateAgainstDef)(resolved.defName, value);
|
|
177
|
+
if (result.valid) {
|
|
178
|
+
output_1.default.green(label);
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
output_1.default.error(label);
|
|
182
|
+
outputErrors(result, ' ');
|
|
183
|
+
return result.errors.length;
|
|
184
|
+
}
|
|
185
|
+
function validateSinglePipeline(parsed, label) {
|
|
186
|
+
let errors = 0;
|
|
187
|
+
const defs = (0, schemaUtils_1.getAllDefs)();
|
|
188
|
+
const pipelineDef = defs['PipelineYaml'];
|
|
189
|
+
const props = pipelineDef?.properties || {};
|
|
190
|
+
const allKnownKeys = new Set([...Object.keys(props), ...SCHEMA_OMITTED_TOP_LEVEL_KEYS]);
|
|
191
|
+
const unknownKeys = Object.keys(parsed).filter((k) => !allKnownKeys.has(k));
|
|
192
|
+
if (unknownKeys.length > 0) {
|
|
193
|
+
output_1.default.yellow(`${label}${(0, texts_1.TXT_YAML_UNKNOWN_TOP_LEVEL_KEYS)(unknownKeys.join(', '))}`);
|
|
194
|
+
}
|
|
195
|
+
const { itemSections, objectSections, settingsProps } = classifySections(props);
|
|
196
|
+
if (Object.keys(settingsProps).length > 0) {
|
|
197
|
+
const settingsData = {};
|
|
198
|
+
for (const key of Object.keys(settingsProps)) {
|
|
199
|
+
if (key in parsed)
|
|
200
|
+
settingsData[key] = parsed[key];
|
|
201
|
+
}
|
|
202
|
+
const rawSchema = (0, schemaUtils_1.loadRawSchema)();
|
|
203
|
+
const result = (0, schemaUtils_1.validateAgainstSchema)({
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: settingsProps,
|
|
206
|
+
required: (pipelineDef?.required || []).filter((r) => r in settingsProps),
|
|
207
|
+
$defs: rawSchema.$defs,
|
|
208
|
+
}, settingsData);
|
|
209
|
+
if (result.valid) {
|
|
210
|
+
output_1.default.green(`${label}Pipeline settings`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
errors += result.errors.length;
|
|
214
|
+
output_1.default.error(`${label}Pipeline settings`);
|
|
215
|
+
outputErrors(result, ' ');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const itemByKey = new Map(itemSections.map((s) => [s.key, s]));
|
|
219
|
+
const objByKey = new Map(objectSections.map((s) => [s.key, s.defName]));
|
|
220
|
+
for (const key of Object.keys(props)) {
|
|
221
|
+
if (!(key in parsed))
|
|
222
|
+
continue;
|
|
223
|
+
const value = parsed[key];
|
|
224
|
+
// A valueless section is an absent one: the backend drops the key on save.
|
|
225
|
+
if (value === null)
|
|
226
|
+
continue;
|
|
227
|
+
const header = `${label}${prettifyKey(key)}`;
|
|
228
|
+
if (itemByKey.has(key)) {
|
|
229
|
+
if (!Array.isArray(value)) {
|
|
230
|
+
errors += reportWrongContainer(header, 'array', value);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
output_1.default.blue(header);
|
|
234
|
+
errors += validateItemSection(defs, value, itemByKey.get(key));
|
|
235
|
+
}
|
|
236
|
+
else if (objByKey.has(key)) {
|
|
237
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
238
|
+
errors += reportWrongContainer(header, 'object', value);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
errors += validateObjectSection(defs, header, value, objByKey.get(key));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return errors;
|
|
245
|
+
}
|
|
246
|
+
function validatePipelineFile(file) {
|
|
247
|
+
const filePath = (0, path_1.resolve)(file);
|
|
248
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
249
|
+
output_1.default.exitError((0, texts_1.ERR_YAML_VALIDATE_FILE_NOT_FOUND)(file));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const YAML = require('yaml');
|
|
253
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
254
|
+
let parsed;
|
|
255
|
+
try {
|
|
256
|
+
parsed = YAML.parse(content);
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
output_1.default.exitError((0, texts_1.ERR_YAML_PARSE)(e.message));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
263
|
+
output_1.default.exitError(texts_1.ERR_YAML_NOT_ARRAY);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(parsed)) {
|
|
267
|
+
output_1.default.exitError(texts_1.ERR_YAML_PLAIN_OBJECT);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (parsed.length === 0) {
|
|
271
|
+
output_1.default.exitError(texts_1.ERR_YAML_EMPTY_ARRAY);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
let errors = 0;
|
|
275
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
276
|
+
const pipeline = parsed[i];
|
|
277
|
+
if (!pipeline || typeof pipeline !== 'object' || Array.isArray(pipeline)) {
|
|
278
|
+
output_1.default.error(` ${(0, texts_1.ERR_YAML_PIPELINE_NOT_OBJECT)(i + 1)}`);
|
|
279
|
+
errors++;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const pipelineLabel = parsed.length > 1 ? `[${pipeline.pipeline || i + 1}] ` : '';
|
|
283
|
+
errors += validateSinglePipeline(pipeline, pipelineLabel);
|
|
284
|
+
}
|
|
285
|
+
output_1.default.normal('');
|
|
286
|
+
if (errors > 0) {
|
|
287
|
+
output_1.default.exitError((0, texts_1.ERR_YAML_VALIDATE_FAILED_COUNT)(errors));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
output_1.default.exitSuccess(texts_1.TXT_YAML_VALIDATE_ALL_PASS);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const commandYamlValidate = (0, utils_1.newCommand)('validate', texts_1.DESC_COMMAND_YAML_VALIDATE);
|
|
294
|
+
commandYamlValidate.argument('<file>', texts_1.OPTION_YAML_VALIDATE_FILE);
|
|
295
|
+
commandYamlValidate.addHelpText('after', `\nEXAMPLES:${texts_1.EXAMPLE_YAML_VALIDATE}`);
|
|
296
|
+
commandYamlValidate.action(async (file) => {
|
|
297
|
+
await (0, schemaUtils_1.ensureYamlSchema)();
|
|
298
|
+
validatePipelineFile(file);
|
|
299
|
+
});
|
|
300
|
+
exports.default = commandYamlValidate;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
const texts_1 = require("../texts");
|
|
8
|
+
const pipeline_1 = __importDefault(require("./yaml/pipeline"));
|
|
9
|
+
const actions_1 = __importDefault(require("./yaml/actions"));
|
|
10
|
+
const validate_1 = __importDefault(require("./yaml/validate"));
|
|
11
|
+
const commandYaml = (0, utils_1.newCommand)('yaml', texts_1.DESC_COMMAND_YAML);
|
|
12
|
+
commandYaml.addCommand(pipeline_1.default);
|
|
13
|
+
commandYaml.addCommand(actions_1.default);
|
|
14
|
+
commandYaml.addCommand(validate_1.default);
|
|
15
|
+
commandYaml.addHelpText('after', `
|
|
16
|
+
EXAMPLES:${texts_1.EXAMPLE_YAML_PIPELINE}
|
|
17
|
+
${texts_1.EXAMPLE_YAML_ACTIONS_LIST}
|
|
18
|
+
${texts_1.EXAMPLE_YAML_ACTIONS_INFO}
|
|
19
|
+
${texts_1.EXAMPLE_YAML_VALIDATE}`);
|
|
20
|
+
exports.default = commandYaml;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CACHE_TTL_MS = void 0;
|
|
7
|
+
exports.cacheRoot = cacheRoot;
|
|
8
|
+
exports.hashKey = hashKey;
|
|
9
|
+
exports.cachePaths = cachePaths;
|
|
10
|
+
exports.readMeta = readMeta;
|
|
11
|
+
exports.isHardFresh = isHardFresh;
|
|
12
|
+
exports.writeAtomic = writeAtomic;
|
|
13
|
+
exports.writeCache = writeCache;
|
|
14
|
+
exports.readCached = readCached;
|
|
15
|
+
exports.sweepStaleCache = sweepStaleCache;
|
|
16
|
+
exports.refresh = refresh;
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const undici_1 = require("undici");
|
|
21
|
+
const utils_1 = require("./utils");
|
|
22
|
+
exports.CACHE_TTL_MS = 60 * 60 * 1000; // 1h
|
|
23
|
+
function ensureDir(dir) {
|
|
24
|
+
if ((0, node_fs_1.existsSync)(dir))
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
28
|
+
(0, utils_1.chownToHomeOwner)(dir);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function cacheRoot() {
|
|
35
|
+
const dir = node_path_1.default.join((0, utils_1.getHomeDirectory)(), 'cache');
|
|
36
|
+
ensureDir(dir);
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
function hashKey(input) {
|
|
40
|
+
return (0, node_crypto_1.createHash)('sha256').update(input).digest('hex').slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
function cachePaths(subdir, url) {
|
|
43
|
+
const dir = node_path_1.default.join(cacheRoot(), subdir);
|
|
44
|
+
ensureDir(dir);
|
|
45
|
+
const key = hashKey(url);
|
|
46
|
+
return {
|
|
47
|
+
raw: node_path_1.default.join(dir, `${key}.json`),
|
|
48
|
+
schema: node_path_1.default.join(dir, `${key}.v8`),
|
|
49
|
+
meta: node_path_1.default.join(dir, `${key}.meta.json`),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function readMeta(metaPath) {
|
|
53
|
+
// `?? ''`, not `?? '{}'`: JSON.parse(null) returns null instead of throwing.
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(readCached(metaPath) ?? '');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function isHardFresh(meta, ttlMs = exports.CACHE_TTL_MS) {
|
|
62
|
+
return !!meta.fetchedAt && Date.now() - meta.fetchedAt < ttlMs;
|
|
63
|
+
}
|
|
64
|
+
function writeAtomic(filePath, data) {
|
|
65
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
66
|
+
try {
|
|
67
|
+
(0, node_fs_1.writeFileSync)(tmp, data);
|
|
68
|
+
(0, node_fs_1.renameSync)(tmp, filePath);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
try {
|
|
72
|
+
(0, node_fs_1.unlinkSync)(tmp);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const memory = new Map();
|
|
81
|
+
function writeCache(filePath, data) {
|
|
82
|
+
try {
|
|
83
|
+
writeAtomic(filePath, data);
|
|
84
|
+
memory.delete(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
memory.set(filePath, data);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function readCached(filePath) {
|
|
93
|
+
const mem = memory.get(filePath);
|
|
94
|
+
if (mem !== undefined)
|
|
95
|
+
return mem;
|
|
96
|
+
try {
|
|
97
|
+
return (0, node_fs_1.readFileSync)(filePath, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const TMP_GRACE_MS = 60 * 60 * 1000; // 1h
|
|
104
|
+
const BLOB_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30d
|
|
105
|
+
function remove(p) {
|
|
106
|
+
try {
|
|
107
|
+
(0, node_fs_1.unlinkSync)(p);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function sweepTmp(dir, now) {
|
|
114
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
115
|
+
if (!entry.isFile() || !entry.name.endsWith('.tmp'))
|
|
116
|
+
continue;
|
|
117
|
+
const p = node_path_1.default.join(dir, entry.name);
|
|
118
|
+
try {
|
|
119
|
+
// Not younger than the grace period: a concurrent bdy is writing its own <pid>.tmp now.
|
|
120
|
+
if (now - (0, node_fs_1.statSync)(p).mtimeMs > TMP_GRACE_MS)
|
|
121
|
+
remove(p);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// ignore
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function sweepBlobs(dir, now) {
|
|
129
|
+
const keys = new Set();
|
|
130
|
+
for (const entry of (0, node_fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
131
|
+
if (entry.isFile() && !entry.name.endsWith('.tmp'))
|
|
132
|
+
keys.add(entry.name.split('.')[0]);
|
|
133
|
+
}
|
|
134
|
+
for (const key of keys) {
|
|
135
|
+
const meta = readMeta(node_path_1.default.join(dir, `${key}.meta.json`));
|
|
136
|
+
let age;
|
|
137
|
+
if (meta.fetchedAt) {
|
|
138
|
+
age = now - meta.fetchedAt;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
try {
|
|
142
|
+
age = now - (0, node_fs_1.statSync)(node_path_1.default.join(dir, `${key}.json`)).mtimeMs;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (age <= BLOB_MAX_AGE_MS)
|
|
149
|
+
continue;
|
|
150
|
+
for (const ext of ['json', 'v8', 'meta.json'])
|
|
151
|
+
remove(node_path_1.default.join(dir, `${key}.${ext}`));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function sweepStaleCache() {
|
|
155
|
+
const root = cacheRoot();
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
try {
|
|
158
|
+
sweepTmp(root, now);
|
|
159
|
+
for (const entry of (0, node_fs_1.readdirSync)(root, { withFileTypes: true })) {
|
|
160
|
+
if (!entry.isDirectory())
|
|
161
|
+
continue;
|
|
162
|
+
const sub = node_path_1.default.join(root, entry.name);
|
|
163
|
+
sweepTmp(sub, now);
|
|
164
|
+
sweepBlobs(sub, now);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// ignore
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function refresh(url, paths) {
|
|
172
|
+
const hasRaw = (0, node_fs_1.existsSync)(paths.raw);
|
|
173
|
+
const meta = hasRaw ? readMeta(paths.meta) : {};
|
|
174
|
+
// The server serves a strong content-based ETag, so revalidate with If-None-Match.
|
|
175
|
+
const headers = {};
|
|
176
|
+
if (hasRaw && meta.etag)
|
|
177
|
+
headers['if-none-match'] = meta.etag;
|
|
178
|
+
try {
|
|
179
|
+
const res = await (0, undici_1.request)(url, { headers });
|
|
180
|
+
const etag = res.headers['etag'];
|
|
181
|
+
if (res.statusCode === 304 && hasRaw) {
|
|
182
|
+
await res.body.dump();
|
|
183
|
+
writeCache(paths.meta, JSON.stringify({ etag: meta.etag, fetchedAt: Date.now() }));
|
|
184
|
+
return 'unchanged';
|
|
185
|
+
}
|
|
186
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
187
|
+
const onDisk = writeCache(paths.raw, await res.body.text());
|
|
188
|
+
// fetchedAt only for a body that landed, or the next run trusts a meta with no raw.
|
|
189
|
+
if (onDisk) {
|
|
190
|
+
writeCache(paths.meta, JSON.stringify({ etag: typeof etag === 'string' ? etag : undefined, fetchedAt: Date.now() }));
|
|
191
|
+
}
|
|
192
|
+
return onDisk ? 'updated' : hasRaw ? 'unchanged' : 'updated-memory';
|
|
193
|
+
}
|
|
194
|
+
await res.body.dump();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// network error — fall back to the cached body if present
|
|
198
|
+
}
|
|
199
|
+
return hasRaw ? 'unchanged' : 'none';
|
|
200
|
+
}
|