clyops-jobs 0.8.0 → 0.10.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/README.md +6 -1
- package/clyops-jobs.cjs +76 -34
- package/dist/artifacts.d.ts +2 -2
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +3 -3
- package/dist/functions.d.ts +5 -1
- package/dist/functions.d.ts.map +1 -1
- package/dist/functions.js +34 -10
- package/dist/template.d.ts +21 -6
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +54 -16
- package/package.json +3 -3
- package/src/artifacts.ts +4 -4
- package/src/functions.ts +37 -10
- package/src/template.ts +59 -18
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ and no `node_modules`. It includes clyops-tools. `require('clyops-jobs')` loads
|
|
|
35
35
|
- `positionals` (optional) lists the tool's positional arguments explicitly; otherwise they're
|
|
36
36
|
options keyed by argument name.
|
|
37
37
|
- String values may use `${dot.path}` or `{{dot.path}}` templates against a context you build
|
|
38
|
-
(`job`, `paths`, whatever the trigger knows).
|
|
38
|
+
(`job`, `paths`, whatever the trigger knows). An unknown path is an error, so a typo fails
|
|
39
|
+
loudly, except under `params` (the caller's fields): an option or positional that names a
|
|
40
|
+
missing or null `params` field is left unset, so the tool's default applies. That holds for
|
|
41
|
+
`${params.x}` alone and inside a longer string (`v-${params.x}`), so a value is never half
|
|
42
|
+
filled. Elsewhere (`result`, `stdin`, `stdout`, artifact patterns) a missing field renders as
|
|
43
|
+
`''`. `strict_params: true` on a function makes a missing `params` field an error too.
|
|
39
44
|
- `artifacts`: glob patterns (`*`, `?`, `**`) relative to `paths.work_dir`, copied into
|
|
40
45
|
`paths.artifacts_dir`, with an optional `required: true` or `min: N`.
|
|
41
46
|
- `stdin`: a template naming a file fed to the tool's stdin (`"${media.path}"`; relative to the
|
package/clyops-jobs.cjs
CHANGED
|
@@ -32,10 +32,12 @@ __export(index_exports, {
|
|
|
32
32
|
deepMerge: () => deepMerge,
|
|
33
33
|
ensureDir: () => ensureDir,
|
|
34
34
|
findFilesByArtifactPattern: () => findFilesByArtifactPattern,
|
|
35
|
+
functionTemplateOptions: () => functionTemplateOptions,
|
|
35
36
|
globToRegex: () => globToRegex,
|
|
36
37
|
isDirectoryPath: () => isDirectoryPath,
|
|
37
38
|
isEnabled: () => isEnabled,
|
|
38
39
|
isFilePath: () => isFilePath,
|
|
40
|
+
isUnsetTemplate: () => isUnsetTemplate,
|
|
39
41
|
listFilesRecursive: () => listFilesRecursive,
|
|
40
42
|
lookupTemplateValue: () => lookupTemplateValue,
|
|
41
43
|
newJobId: () => newJobId,
|
|
@@ -135,40 +137,59 @@ function resolveConfigPath(root, value) {
|
|
|
135
137
|
var resolveScriptPath = resolveConfigPath;
|
|
136
138
|
|
|
137
139
|
// packages/jobs/src/template.ts
|
|
138
|
-
|
|
140
|
+
var OPTIONAL_ROOTS = ["params"];
|
|
141
|
+
var TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
142
|
+
var MISSING = /* @__PURE__ */ Symbol("missing");
|
|
143
|
+
function resolvePath(context, expression) {
|
|
139
144
|
let current = context;
|
|
140
145
|
for (const part of String(expression).split(".").filter(Boolean)) {
|
|
141
|
-
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part))
|
|
142
|
-
throw new Error(`unknown template value: ${expression}`);
|
|
143
|
-
}
|
|
146
|
+
if (!current || typeof current !== "object" || !Object.prototype.hasOwnProperty.call(current, part)) return MISSING;
|
|
144
147
|
current = current[part];
|
|
145
148
|
}
|
|
149
|
+
return current;
|
|
150
|
+
}
|
|
151
|
+
function optionalRoot(expression, opts) {
|
|
152
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split(".").filter(Boolean)[0] ?? "");
|
|
153
|
+
}
|
|
154
|
+
function lookupTemplateValue(context, expression, opts) {
|
|
155
|
+
const current = resolvePath(context, expression);
|
|
156
|
+
if (current === MISSING) {
|
|
157
|
+
if (optionalRoot(expression, opts)) return "";
|
|
158
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
159
|
+
}
|
|
146
160
|
if (current === void 0 || current === null) return "";
|
|
147
161
|
return typeof current === "object" ? JSON.stringify(current) : String(current);
|
|
148
162
|
}
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
function isUnsetTemplate(value, context, opts) {
|
|
164
|
+
if (typeof value !== "string") return false;
|
|
165
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
166
|
+
const expression = dollar || brace;
|
|
167
|
+
if (!optionalRoot(expression, opts)) continue;
|
|
168
|
+
const current = resolvePath(context, expression);
|
|
169
|
+
if (current === MISSING || current === void 0 || current === null) return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
function renderTemplateString(value, context, opts) {
|
|
174
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
154
175
|
}
|
|
155
|
-
function renderConfigValue(value, context) {
|
|
176
|
+
function renderConfigValue(value, context, opts) {
|
|
156
177
|
if (!context) return value;
|
|
157
|
-
if (typeof value === "string") return renderTemplateString(value, context);
|
|
158
|
-
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context));
|
|
178
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
179
|
+
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context, opts));
|
|
159
180
|
return value;
|
|
160
181
|
}
|
|
161
|
-
function renderResultValue(value, context) {
|
|
162
|
-
if (typeof value === "string") return renderTemplateString(value, context);
|
|
163
|
-
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context));
|
|
182
|
+
function renderResultValue(value, context, opts) {
|
|
183
|
+
if (typeof value === "string") return renderTemplateString(value, context, opts);
|
|
184
|
+
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context, opts));
|
|
164
185
|
if (value && typeof value === "object") {
|
|
165
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
186
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
166
187
|
}
|
|
167
188
|
return value;
|
|
168
189
|
}
|
|
169
|
-
function renderResultMap(resultMap, context) {
|
|
190
|
+
function renderResultMap(resultMap, context, opts) {
|
|
170
191
|
if (!resultMap || typeof resultMap !== "object" || Array.isArray(resultMap)) return {};
|
|
171
|
-
return renderResultValue(resultMap, context);
|
|
192
|
+
return renderResultValue(resultMap, context, opts);
|
|
172
193
|
}
|
|
173
194
|
|
|
174
195
|
// packages/tools/src/schema.ts
|
|
@@ -179,12 +200,13 @@ function stamp(file) {
|
|
|
179
200
|
return `${s.mtimeMs}:${s.size}`;
|
|
180
201
|
}
|
|
181
202
|
var cache = /* @__PURE__ */ new Map();
|
|
203
|
+
var PROBE_TIMEOUT_MS = 3e4;
|
|
182
204
|
function loadSchema(file, opts = {}) {
|
|
183
205
|
const key = stamp(file);
|
|
184
206
|
const hit = cache.get(file);
|
|
185
207
|
if (hit && hit.stamp === key) return hit.schema;
|
|
186
208
|
const schema = new Promise((resolve5, reject) => {
|
|
187
|
-
(0, import_node_child_process.execFile)(file, ["--help-json-schema"], { cwd: opts.cwd, timeout: opts.timeoutMs ??
|
|
209
|
+
(0, import_node_child_process.execFile)(file, ["--help-json-schema"], { cwd: opts.cwd, timeout: opts.timeoutMs ?? PROBE_TIMEOUT_MS, maxBuffer: 16 << 20 }, (err, stdout) => {
|
|
188
210
|
if (err) return reject(new Error(`${file} does not provide --help-json-schema: ${err.message}`));
|
|
189
211
|
const start2 = stdout.indexOf("{");
|
|
190
212
|
const end = stdout.lastIndexOf("}");
|
|
@@ -200,7 +222,9 @@ function loadSchema(file, opts = {}) {
|
|
|
200
222
|
}
|
|
201
223
|
});
|
|
202
224
|
});
|
|
203
|
-
schema.catch(() =>
|
|
225
|
+
schema.catch(() => {
|
|
226
|
+
if (cache.get(file)?.schema === schema) cache.delete(file);
|
|
227
|
+
});
|
|
204
228
|
cache.set(file, { stamp: key, schema });
|
|
205
229
|
return schema;
|
|
206
230
|
}
|
|
@@ -233,6 +257,9 @@ function isPathValued(validation, type) {
|
|
|
233
257
|
function isBareProgram(validation, value) {
|
|
234
258
|
return validation === "file:executable" && value !== "" && !value.includes("/") && !(process.platform === "win32" && value.includes("\\"));
|
|
235
259
|
}
|
|
260
|
+
function oneJsonValue(validation, many) {
|
|
261
|
+
return (validation === "json" || validation.startsWith("json:")) && !many;
|
|
262
|
+
}
|
|
236
263
|
var TRUE = /^(true|1|yes|on)$/i;
|
|
237
264
|
var FALSE = /^(false|0|no|off)$/i;
|
|
238
265
|
function toArgv(schema, input, opts = {}) {
|
|
@@ -280,7 +307,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
280
307
|
out.env[option.variableName] = text(found.value, isPathValued(option.validation, option.type), `--${option.name}`, option.validation);
|
|
281
308
|
continue;
|
|
282
309
|
}
|
|
283
|
-
for (const item of Array.isArray(found.value) ? found.value : [found.value]) {
|
|
310
|
+
for (const item of Array.isArray(found.value) && !oneJsonValue(option.validation, option.isArray) ? found.value : [found.value]) {
|
|
284
311
|
out.argv.push(`--${option.name}`, text(item, isPathValued(option.validation, option.type), `--${option.name}`, option.validation));
|
|
285
312
|
}
|
|
286
313
|
}
|
|
@@ -306,7 +333,7 @@ function toArgv(schema, input, opts = {}) {
|
|
|
306
333
|
positionals.push(text(gap.default, false));
|
|
307
334
|
}
|
|
308
335
|
skipped = [];
|
|
309
|
-
const values = Array.isArray(found.value) ? found.value : [found.value];
|
|
336
|
+
const values = Array.isArray(found.value) && !oneJsonValue(argument.validation, argument.isVariadic) ? found.value : [found.value];
|
|
310
337
|
positionals.push(...values.map((v) => text(v, isPathValued(argument.validation), argument.name, argument.validation)));
|
|
311
338
|
}
|
|
312
339
|
}
|
|
@@ -597,22 +624,35 @@ function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
597
624
|
const config = deepMerge(objectValue(objectValue(routeConfig.defaults)[key]), objectValue(routeConfig[key]));
|
|
598
625
|
return { functionName, key, script: resolveScriptPath(scriptRoot, script), config, definition };
|
|
599
626
|
}
|
|
627
|
+
function functionTemplateOptions(fn) {
|
|
628
|
+
return { strict: fn.definition.strict_params === true };
|
|
629
|
+
}
|
|
630
|
+
function withoutUnsetParams(config, context, opts) {
|
|
631
|
+
const out = {};
|
|
632
|
+
for (const [key, value] of Object.entries(config)) {
|
|
633
|
+
if (isUnsetTemplate(value, context, opts)) continue;
|
|
634
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
600
638
|
function explicitPositionals(fn, context) {
|
|
601
639
|
if (!Array.isArray(fn.definition.positionals)) return void 0;
|
|
602
|
-
|
|
640
|
+
const templates = functionTemplateOptions(fn);
|
|
641
|
+
return fn.definition.positionals.filter((item) => !context || !isUnsetTemplate(item, context, templates)).map((item) => String(renderConfigValue(item, context, templates))).filter(Boolean);
|
|
603
642
|
}
|
|
604
643
|
async function buildFunctionCommand(fn, opts, schema) {
|
|
605
644
|
schema ??= await loadSchema(fn.script);
|
|
606
|
-
const
|
|
645
|
+
const context = opts.context;
|
|
646
|
+
const templates = functionTemplateOptions(fn);
|
|
647
|
+
const config = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
607
648
|
const first = schema.arguments[0];
|
|
608
649
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
609
650
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
610
651
|
}
|
|
611
|
-
const context = opts.context;
|
|
612
652
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
613
653
|
base: opts.configRoot,
|
|
614
654
|
controlled: opts.controlled ?? ["help"],
|
|
615
|
-
render: context ? (s) => renderTemplateString(s, context) : void 0,
|
|
655
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : void 0,
|
|
616
656
|
positionals: explicitPositionals(fn, context),
|
|
617
657
|
positionalsOrder: fn.definition.positionals_order
|
|
618
658
|
});
|
|
@@ -629,9 +669,9 @@ function parseRoleJson(text) {
|
|
|
629
669
|
return void 0;
|
|
630
670
|
}
|
|
631
671
|
}
|
|
632
|
-
function streamPath(template, opts, base) {
|
|
672
|
+
function streamPath(template, opts, base, templates) {
|
|
633
673
|
if (!template) return void 0;
|
|
634
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
674
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
635
675
|
return (0, import_node_path3.isAbsolute)(rendered) ? rendered : (0, import_node_path3.resolve)(base, rendered);
|
|
636
676
|
}
|
|
637
677
|
async function runScriptFunction(fn, opts) {
|
|
@@ -640,8 +680,8 @@ async function runScriptFunction(fn, opts) {
|
|
|
640
680
|
const command = [file, ...redactArgv(schema, argv)];
|
|
641
681
|
LOGGER.verbose("command: %s", shellQuote(command));
|
|
642
682
|
const workDir = opts.context?.paths?.work_dir;
|
|
643
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
644
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
683
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
684
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
645
685
|
let progress;
|
|
646
686
|
const roles = {
|
|
647
687
|
progress: {
|
|
@@ -685,7 +725,7 @@ function buildResultRecord(p) {
|
|
|
685
725
|
artifact_keys: p.artifactKeys,
|
|
686
726
|
completed_at: p.now,
|
|
687
727
|
config: p.fn.config,
|
|
688
|
-
...renderResultMap(p.fn.definition.result, p.context)
|
|
728
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn))
|
|
689
729
|
};
|
|
690
730
|
}
|
|
691
731
|
|
|
@@ -731,8 +771,8 @@ function listFilesRecursive(dir, visited = /* @__PURE__ */ new Set()) {
|
|
|
731
771
|
}
|
|
732
772
|
return files;
|
|
733
773
|
}
|
|
734
|
-
function findFilesByArtifactPattern(pattern, context) {
|
|
735
|
-
const rendered = renderTemplateString(pattern, context);
|
|
774
|
+
function findFilesByArtifactPattern(pattern, context, opts) {
|
|
775
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
736
776
|
const absolute = (0, import_node_path4.isAbsolute)(rendered) ? rendered : (0, import_node_path4.resolve)(context.paths.work_dir, rendered);
|
|
737
777
|
if (!/[*?]/.test(absolute)) return isFilePath(absolute) ? [absolute] : [];
|
|
738
778
|
const regex = globToRegex(absolute);
|
|
@@ -771,7 +811,7 @@ function copyConfiguredArtifacts(fn, context, opts = {}) {
|
|
|
771
811
|
const seen = /* @__PURE__ */ new Set();
|
|
772
812
|
const copied = [];
|
|
773
813
|
for (const pattern of patterns) {
|
|
774
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
814
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
775
815
|
if (seen.has(source)) continue;
|
|
776
816
|
seen.add(source);
|
|
777
817
|
const destination = (0, import_node_path4.join)(dir, (0, import_node_path4.basename)(source));
|
|
@@ -921,10 +961,12 @@ var JobQueue = class {
|
|
|
921
961
|
deepMerge,
|
|
922
962
|
ensureDir,
|
|
923
963
|
findFilesByArtifactPattern,
|
|
964
|
+
functionTemplateOptions,
|
|
924
965
|
globToRegex,
|
|
925
966
|
isDirectoryPath,
|
|
926
967
|
isEnabled,
|
|
927
968
|
isFilePath,
|
|
969
|
+
isUnsetTemplate,
|
|
928
970
|
listFilesRecursive,
|
|
929
971
|
lookupTemplateValue,
|
|
930
972
|
newJobId,
|
package/dist/artifacts.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedFunction } from './functions.js';
|
|
2
|
-
import { type TemplateContext } from './template.js';
|
|
2
|
+
import { type TemplateContext, type TemplateOptions } from './template.js';
|
|
3
3
|
/** Compile an artifact glob into an anchored regular expression. */
|
|
4
4
|
export declare function globToRegex(pattern: string): RegExp;
|
|
5
5
|
/** Every file below `dir`, following symlinks without looping. */
|
|
@@ -9,7 +9,7 @@ export declare function findFilesByArtifactPattern(pattern: string, context: Tem
|
|
|
9
9
|
paths: {
|
|
10
10
|
work_dir: string;
|
|
11
11
|
};
|
|
12
|
-
}): string[];
|
|
12
|
+
}, opts?: TemplateOptions): string[];
|
|
13
13
|
/** A function's `artifacts` as a list of include patterns, plus its `stdout` file. */
|
|
14
14
|
export declare function artifactIncludePatterns(fn: ResolvedFunction): string[];
|
|
15
15
|
/** How many artifacts a function must produce: `min`, or 1 for `required: true`. */
|
package/dist/artifacts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../src/artifacts.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAwB,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAKjG,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAiBnD;AAWD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,cAAoB,GAAG,MAAM,EAAE,CAarF;AAED,oFAAoF;AACpF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,EAAE,CAQxJ;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAatE;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,gBAAgB,GAAG,MAAM,CAMhE;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,gBAAgB,EACpB,OAAO,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,EACjF,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,cAAc,EAAE,CAwBlB"}
|
package/dist/artifacts.js
CHANGED
|
@@ -59,8 +59,8 @@ export function listFilesRecursive(dir, visited = new Set()) {
|
|
|
59
59
|
return files;
|
|
60
60
|
}
|
|
61
61
|
/** Files matching a templated glob; relative patterns start at `paths.work_dir`. */
|
|
62
|
-
export function findFilesByArtifactPattern(pattern, context) {
|
|
63
|
-
const rendered = renderTemplateString(pattern, context);
|
|
62
|
+
export function findFilesByArtifactPattern(pattern, context, opts) {
|
|
63
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
64
64
|
const absolute = isAbsolute(rendered) ? rendered : resolve(context.paths.work_dir, rendered);
|
|
65
65
|
if (!/[*?]/.test(absolute))
|
|
66
66
|
return isFilePath(absolute) ? [absolute] : [];
|
|
@@ -118,7 +118,7 @@ export function copyConfiguredArtifacts(fn, context, opts = {}) {
|
|
|
118
118
|
const seen = new Set();
|
|
119
119
|
const copied = [];
|
|
120
120
|
for (const pattern of patterns) {
|
|
121
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
121
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
122
122
|
if (seen.has(source))
|
|
123
123
|
continue;
|
|
124
124
|
seen.add(source);
|
package/dist/functions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RunResult, type Schema } from 'clyops-tools';
|
|
2
|
-
import { type TemplateContext } from './template.js';
|
|
2
|
+
import { type TemplateContext, type TemplateOptions } from './template.js';
|
|
3
3
|
import { type Json } from './util.js';
|
|
4
4
|
/**
|
|
5
5
|
* Route diagnostics. `warn` and `verbose` take a printf-style format and
|
|
@@ -29,6 +29,8 @@ export interface FunctionDefinition {
|
|
|
29
29
|
artifacts?: unknown;
|
|
30
30
|
/** Task result or configured result templates, according to the containing interface. */
|
|
31
31
|
result?: unknown;
|
|
32
|
+
/** A missing `params` field in a template is an error instead of unset (default false). */
|
|
33
|
+
strict_params?: boolean;
|
|
32
34
|
[key: string]: unknown;
|
|
33
35
|
}
|
|
34
36
|
/** A configured function with resolved executable and merged defaults and overrides. */
|
|
@@ -63,6 +65,8 @@ export interface CommandOptions {
|
|
|
63
65
|
*/
|
|
64
66
|
defaultPositional?: string;
|
|
65
67
|
}
|
|
68
|
+
/** The template options a function's definition asks for. */
|
|
69
|
+
export declare function functionTemplateOptions(fn: ResolvedFunction): TemplateOptions;
|
|
66
70
|
/** The full command line for a function: [script, ...argv]. */
|
|
67
71
|
export declare function buildFunctionCommand(fn: ResolvedFunction, opts: CommandOptions, schema?: Schema): Promise<string[]>;
|
|
68
72
|
/** The completed tool execution with a color-stripped stderr tail. */
|
package/dist/functions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"AASA,OAAO,EAAsG,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAI/J,OAAO,
|
|
1
|
+
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../src/functions.ts"],"names":[],"mappings":"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"}
|
package/dist/functions.js
CHANGED
|
@@ -11,7 +11,7 @@ import { declaredRoles, inputKeys, loadSchema, redactArgv, start, shellQuote, ta
|
|
|
11
11
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
12
|
import { finished } from 'node:stream/promises';
|
|
13
13
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
14
|
-
import { renderConfigValue, renderResultMap, renderTemplateString } from './template.js';
|
|
14
|
+
import { isUnsetTemplate, renderConfigValue, renderResultMap, renderTemplateString } from './template.js';
|
|
15
15
|
import { deepMerge, ensureDir, objectValue, resolveScriptPath } from './util.js';
|
|
16
16
|
/** Where engine diagnostics go; a trigger installs its own with configureLogging. */
|
|
17
17
|
const LOGGER = {
|
|
@@ -42,6 +42,24 @@ export function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
42
42
|
const config = deepMerge(objectValue(objectValue(routeConfig.defaults)[key]), objectValue(routeConfig[key]));
|
|
43
43
|
return { functionName, key, script: resolveScriptPath(scriptRoot, script), config, definition };
|
|
44
44
|
}
|
|
45
|
+
/** The template options a function's definition asks for. */
|
|
46
|
+
export function functionTemplateOptions(fn) {
|
|
47
|
+
return { strict: fn.definition.strict_params === true };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The options with `params` templates that name a missing or null field left
|
|
51
|
+
* out, so the tool's own default applies: such an option is dropped, alone or
|
|
52
|
+
* inside a longer string, and so is such an item in an array.
|
|
53
|
+
*/
|
|
54
|
+
function withoutUnsetParams(config, context, opts) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, value] of Object.entries(config)) {
|
|
57
|
+
if (isUnsetTemplate(value, context, opts))
|
|
58
|
+
continue;
|
|
59
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
45
63
|
/**
|
|
46
64
|
* The positionals for a function: its `positionals` templates when set
|
|
47
65
|
* (authoritative; `[]` for none), else none here and the schema's arguments
|
|
@@ -50,21 +68,27 @@ export function resolveFunctionConfig(scriptRoot, routeConfig, functionName) {
|
|
|
50
68
|
function explicitPositionals(fn, context) {
|
|
51
69
|
if (!Array.isArray(fn.definition.positionals))
|
|
52
70
|
return undefined;
|
|
53
|
-
|
|
71
|
+
const templates = functionTemplateOptions(fn);
|
|
72
|
+
return fn.definition.positionals
|
|
73
|
+
.filter((item) => !context || !isUnsetTemplate(item, context, templates))
|
|
74
|
+
.map((item) => String(renderConfigValue(item, context, templates)))
|
|
75
|
+
.filter(Boolean);
|
|
54
76
|
}
|
|
55
77
|
/** The full command line for a function: [script, ...argv]. */
|
|
56
78
|
export async function buildFunctionCommand(fn, opts, schema) {
|
|
57
79
|
schema ??= await loadSchema(fn.script);
|
|
58
|
-
const
|
|
80
|
+
const context = opts.context;
|
|
81
|
+
const templates = functionTemplateOptions(fn);
|
|
82
|
+
// First, so an argument left unset still takes the default positional.
|
|
83
|
+
const config = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
59
84
|
const first = schema.arguments[0];
|
|
60
85
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
61
86
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
62
87
|
}
|
|
63
|
-
const context = opts.context;
|
|
64
88
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
65
89
|
base: opts.configRoot,
|
|
66
90
|
controlled: opts.controlled ?? ['help'],
|
|
67
|
-
render: context ? (s) => renderTemplateString(s, context) : undefined,
|
|
91
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : undefined,
|
|
68
92
|
positionals: explicitPositionals(fn, context),
|
|
69
93
|
positionalsOrder: fn.definition.positionals_order,
|
|
70
94
|
});
|
|
@@ -87,10 +111,10 @@ export function parseRoleJson(text) {
|
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
/** A function's `stdin` or `stdout` template rendered, a relative one against `base`. */
|
|
90
|
-
function streamPath(template, opts, base) {
|
|
114
|
+
function streamPath(template, opts, base, templates) {
|
|
91
115
|
if (!template)
|
|
92
116
|
return undefined;
|
|
93
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
117
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
94
118
|
return isAbsolute(rendered) ? rendered : resolve(base, rendered);
|
|
95
119
|
}
|
|
96
120
|
/**
|
|
@@ -105,8 +129,8 @@ export async function runScriptFunction(fn, opts) {
|
|
|
105
129
|
LOGGER.verbose('command: %s', shellQuote(command));
|
|
106
130
|
// stdin is input, like a path option; stdout is an artifact, found like one.
|
|
107
131
|
const workDir = opts.context?.paths?.work_dir;
|
|
108
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
109
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
132
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
133
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
110
134
|
// Wire the well-known roles the tool declares: progress onto the record, meta into the run.
|
|
111
135
|
let progress;
|
|
112
136
|
const roles = {
|
|
@@ -148,6 +172,6 @@ export function buildResultRecord(p) {
|
|
|
148
172
|
artifact_keys: p.artifactKeys,
|
|
149
173
|
completed_at: p.now,
|
|
150
174
|
config: p.fn.config,
|
|
151
|
-
...renderResultMap(p.fn.definition.result, p.context),
|
|
175
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn)),
|
|
152
176
|
};
|
|
153
177
|
}
|
package/dist/template.d.ts
CHANGED
|
@@ -2,16 +2,31 @@
|
|
|
2
2
|
export type TemplateContext = {
|
|
3
3
|
[key: string]: unknown;
|
|
4
4
|
};
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
5
|
+
/** How templates treat missing values. */
|
|
6
|
+
export interface TemplateOptions {
|
|
7
|
+
/** Every unknown path is an error, `params` too (default false: a missing `params` field renders `''`, and an option naming one is unset). */
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
12
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
13
|
+
* unless `opts.strict`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function lookupTemplateValue(context: TemplateContext, expression: string, opts?: TemplateOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
18
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
19
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isUnsetTemplate(value: unknown, context: TemplateContext, opts?: TemplateOptions): boolean;
|
|
7
22
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
8
|
-
export declare function renderTemplateString(value: string, context: TemplateContext): string;
|
|
23
|
+
export declare function renderTemplateString(value: string, context: TemplateContext, opts?: TemplateOptions): string;
|
|
9
24
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
10
|
-
export declare function renderConfigValue(value: unknown, context?: TemplateContext | null): unknown;
|
|
25
|
+
export declare function renderConfigValue(value: unknown, context?: TemplateContext | null, opts?: TemplateOptions): unknown;
|
|
11
26
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
12
|
-
export declare function renderResultValue(value: unknown, context: TemplateContext): unknown;
|
|
27
|
+
export declare function renderResultValue(value: unknown, context: TemplateContext, opts?: TemplateOptions): unknown;
|
|
13
28
|
/** Render a function's optional `result` map into extra result fields. */
|
|
14
|
-
export declare function renderResultMap(resultMap: unknown, context: TemplateContext): {
|
|
29
|
+
export declare function renderResultMap(resultMap: unknown, context: TemplateContext, opts?: TemplateOptions): {
|
|
15
30
|
[key: string]: unknown;
|
|
16
31
|
};
|
|
17
32
|
//# sourceMappingURL=template.d.ts.map
|
package/dist/template.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AACA,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAEzD,
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AACA,4DAA4D;AAC5D,MAAM,MAAM,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAEzD,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,8IAA8I;IAC9I,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAqBD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAQhH;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CASzG;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,MAAM,CAE5G;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAKnH;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAO3G;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAGhI"}
|
package/dist/template.js
CHANGED
|
@@ -1,44 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Caller-supplied fields: one a request leaves out is unset, not a config error.
|
|
2
|
+
const OPTIONAL_ROOTS = ['params'];
|
|
3
|
+
const TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
4
|
+
// The value at a dot path, or MISSING when the path is not there.
|
|
5
|
+
const MISSING = Symbol('missing');
|
|
6
|
+
function resolvePath(context, expression) {
|
|
3
7
|
let current = context;
|
|
4
8
|
for (const part of String(expression).split('.').filter(Boolean)) {
|
|
5
|
-
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
6
|
-
|
|
7
|
-
}
|
|
9
|
+
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
10
|
+
return MISSING;
|
|
8
11
|
current = current[part];
|
|
9
12
|
}
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
function optionalRoot(expression, opts) {
|
|
16
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split('.').filter(Boolean)[0] ?? '');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
20
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
21
|
+
* unless `opts.strict`.
|
|
22
|
+
*/
|
|
23
|
+
export function lookupTemplateValue(context, expression, opts) {
|
|
24
|
+
const current = resolvePath(context, expression);
|
|
25
|
+
if (current === MISSING) {
|
|
26
|
+
if (optionalRoot(expression, opts))
|
|
27
|
+
return '';
|
|
28
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
29
|
+
}
|
|
10
30
|
if (current === undefined || current === null)
|
|
11
31
|
return '';
|
|
12
32
|
return typeof current === 'object' ? JSON.stringify(current) : String(current);
|
|
13
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
36
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
37
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
38
|
+
*/
|
|
39
|
+
export function isUnsetTemplate(value, context, opts) {
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
43
|
+
const expression = dollar || brace;
|
|
44
|
+
if (!optionalRoot(expression, opts))
|
|
45
|
+
continue;
|
|
46
|
+
const current = resolvePath(context, expression);
|
|
47
|
+
if (current === MISSING || current === undefined || current === null)
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
14
52
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
15
|
-
export function renderTemplateString(value, context) {
|
|
16
|
-
return String(value).replace(
|
|
53
|
+
export function renderTemplateString(value, context, opts) {
|
|
54
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
17
55
|
}
|
|
18
56
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
19
|
-
export function renderConfigValue(value, context) {
|
|
57
|
+
export function renderConfigValue(value, context, opts) {
|
|
20
58
|
if (!context)
|
|
21
59
|
return value;
|
|
22
60
|
if (typeof value === 'string')
|
|
23
|
-
return renderTemplateString(value, context);
|
|
61
|
+
return renderTemplateString(value, context, opts);
|
|
24
62
|
if (Array.isArray(value))
|
|
25
|
-
return value.map((item) => renderConfigValue(item, context));
|
|
63
|
+
return value.map((item) => renderConfigValue(item, context, opts));
|
|
26
64
|
return value;
|
|
27
65
|
}
|
|
28
66
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
29
|
-
export function renderResultValue(value, context) {
|
|
67
|
+
export function renderResultValue(value, context, opts) {
|
|
30
68
|
if (typeof value === 'string')
|
|
31
|
-
return renderTemplateString(value, context);
|
|
69
|
+
return renderTemplateString(value, context, opts);
|
|
32
70
|
if (Array.isArray(value))
|
|
33
|
-
return value.map((item) => renderResultValue(item, context));
|
|
71
|
+
return value.map((item) => renderResultValue(item, context, opts));
|
|
34
72
|
if (value && typeof value === 'object') {
|
|
35
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
73
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
36
74
|
}
|
|
37
75
|
return value;
|
|
38
76
|
}
|
|
39
77
|
/** Render a function's optional `result` map into extra result fields. */
|
|
40
|
-
export function renderResultMap(resultMap, context) {
|
|
78
|
+
export function renderResultMap(resultMap, context, opts) {
|
|
41
79
|
if (!resultMap || typeof resultMap !== 'object' || Array.isArray(resultMap))
|
|
42
80
|
return {};
|
|
43
|
-
return renderResultValue(resultMap, context);
|
|
81
|
+
return renderResultValue(resultMap, context, opts);
|
|
44
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clyops-jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Run clyops tools as jobs: config-bound functions, templated invocation, artifacts, job records and a queue.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"typescript": "^5.6.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"clyops-tools": "0.
|
|
41
|
+
"clyops-tools": "0.10.0"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://clyops.io/node-tools.html",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc -p tsconfig.json",
|
|
46
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
-
"test": "node --test test/*.test.mjs"
|
|
47
|
+
"test": "node --import ../../tools/test-leaks.mjs --test test/*.test.mjs"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/artifacts.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { basename, dirname, isAbsolute, join, normalize, parse, resolve } from 'node:path';
|
|
4
4
|
import type { ResolvedFunction } from './functions.js';
|
|
5
|
-
import { renderTemplateString, type TemplateContext } from './template.js';
|
|
5
|
+
import { renderTemplateString, type TemplateContext, type TemplateOptions } from './template.js';
|
|
6
6
|
import { copyFileEnsuringDir, isDirectoryPath, isFilePath, statFollowingSymlinks, truthy } from './util.js';
|
|
7
7
|
|
|
8
8
|
// A small glob subset as a regex: `*`, `?`, `**` and `**/` (which also matches no
|
|
@@ -53,8 +53,8 @@ export function listFilesRecursive(dir: string, visited = new Set<string>()): st
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Files matching a templated glob; relative patterns start at `paths.work_dir`. */
|
|
56
|
-
export function findFilesByArtifactPattern(pattern: string, context: TemplateContext & { paths: { work_dir: string } }): string[] {
|
|
57
|
-
const rendered = renderTemplateString(pattern, context);
|
|
56
|
+
export function findFilesByArtifactPattern(pattern: string, context: TemplateContext & { paths: { work_dir: string } }, opts?: TemplateOptions): string[] {
|
|
57
|
+
const rendered = renderTemplateString(pattern, context, opts);
|
|
58
58
|
const absolute = isAbsolute(rendered) ? rendered : resolve(context.paths.work_dir, rendered);
|
|
59
59
|
if (!/[*?]/.test(absolute)) return isFilePath(absolute) ? [absolute] : [];
|
|
60
60
|
const regex = globToRegex(absolute);
|
|
@@ -118,7 +118,7 @@ export function copyConfiguredArtifacts(
|
|
|
118
118
|
const seen = new Set<string>();
|
|
119
119
|
const copied: CopiedArtifact[] = [];
|
|
120
120
|
for (const pattern of patterns) {
|
|
121
|
-
for (const source of findFilesByArtifactPattern(pattern, context)) {
|
|
121
|
+
for (const source of findFilesByArtifactPattern(pattern, context, { strict: fn.definition.strict_params === true })) {
|
|
122
122
|
if (seen.has(source)) continue;
|
|
123
123
|
seen.add(source);
|
|
124
124
|
const destination = join(dir, basename(source));
|
package/src/functions.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { declaredRoles, inputKeys, loadSchema, redactArgv, start, shellQuote, ta
|
|
|
11
11
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
12
12
|
import { finished } from 'node:stream/promises';
|
|
13
13
|
import { basename, dirname, isAbsolute, resolve } from 'node:path';
|
|
14
|
-
import { renderConfigValue, renderResultMap, renderTemplateString, type TemplateContext } from './template.js';
|
|
14
|
+
import { isUnsetTemplate, renderConfigValue, renderResultMap, renderTemplateString, type TemplateContext, type TemplateOptions } from './template.js';
|
|
15
15
|
import { deepMerge, ensureDir, objectValue, resolveScriptPath, type Json } from './util.js';
|
|
16
16
|
|
|
17
17
|
/** Where engine diagnostics go; a trigger installs its own with configureLogging. */
|
|
@@ -48,6 +48,8 @@ export interface FunctionDefinition {
|
|
|
48
48
|
artifacts?: unknown;
|
|
49
49
|
/** Task result or configured result templates, according to the containing interface. */
|
|
50
50
|
result?: unknown;
|
|
51
|
+
/** A missing `params` field in a template is an error instead of unset (default false). */
|
|
52
|
+
strict_params?: boolean;
|
|
51
53
|
[key: string]: unknown;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -94,6 +96,25 @@ export interface CommandOptions {
|
|
|
94
96
|
defaultPositional?: string;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/** The template options a function's definition asks for. */
|
|
100
|
+
export function functionTemplateOptions(fn: ResolvedFunction): TemplateOptions {
|
|
101
|
+
return { strict: fn.definition.strict_params === true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The options with `params` templates that name a missing or null field left
|
|
106
|
+
* out, so the tool's own default applies: such an option is dropped, alone or
|
|
107
|
+
* inside a longer string, and so is such an item in an array.
|
|
108
|
+
*/
|
|
109
|
+
function withoutUnsetParams(config: Json, context: TemplateContext, opts: TemplateOptions): Json {
|
|
110
|
+
const out: Json = {};
|
|
111
|
+
for (const [key, value] of Object.entries(config)) {
|
|
112
|
+
if (isUnsetTemplate(value, context, opts)) continue;
|
|
113
|
+
out[key] = Array.isArray(value) ? value.filter((item) => !isUnsetTemplate(item, context, opts)) : value;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
/**
|
|
98
119
|
* The positionals for a function: its `positionals` templates when set
|
|
99
120
|
* (authoritative; `[]` for none), else none here and the schema's arguments
|
|
@@ -101,22 +122,28 @@ export interface CommandOptions {
|
|
|
101
122
|
*/
|
|
102
123
|
function explicitPositionals(fn: ResolvedFunction, context?: TemplateContext | null): string[] | undefined {
|
|
103
124
|
if (!Array.isArray(fn.definition.positionals)) return undefined;
|
|
104
|
-
|
|
125
|
+
const templates = functionTemplateOptions(fn);
|
|
126
|
+
return fn.definition.positionals
|
|
127
|
+
.filter((item) => !context || !isUnsetTemplate(item, context, templates))
|
|
128
|
+
.map((item) => String(renderConfigValue(item, context, templates)))
|
|
129
|
+
.filter(Boolean);
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
/** The full command line for a function: [script, ...argv]. */
|
|
108
133
|
export async function buildFunctionCommand(fn: ResolvedFunction, opts: CommandOptions, schema?: Schema): Promise<string[]> {
|
|
109
134
|
schema ??= await loadSchema(fn.script);
|
|
110
|
-
const
|
|
135
|
+
const context = opts.context;
|
|
136
|
+
const templates = functionTemplateOptions(fn);
|
|
137
|
+
// First, so an argument left unset still takes the default positional.
|
|
138
|
+
const config: Json = context ? withoutUnsetParams(fn.config, context, templates) : { ...fn.config };
|
|
111
139
|
const first = schema.arguments[0];
|
|
112
140
|
if (opts.defaultPositional && first && !Array.isArray(fn.definition.positionals) && !inputKeys(first).some((k) => k in config)) {
|
|
113
141
|
config[inputKeys(first)[0]] = opts.defaultPositional;
|
|
114
142
|
}
|
|
115
|
-
const context = opts.context;
|
|
116
143
|
const { argv, unknown, controlled } = toArgv(schema, config, {
|
|
117
144
|
base: opts.configRoot,
|
|
118
145
|
controlled: opts.controlled ?? ['help'],
|
|
119
|
-
render: context ? (s) => renderTemplateString(s, context) : undefined,
|
|
146
|
+
render: context ? (s) => renderTemplateString(s, context, templates) : undefined,
|
|
120
147
|
positionals: explicitPositionals(fn, context),
|
|
121
148
|
positionalsOrder: fn.definition.positionals_order,
|
|
122
149
|
});
|
|
@@ -147,9 +174,9 @@ export function parseRoleJson(text: string | undefined): unknown {
|
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/** A function's `stdin` or `stdout` template rendered, a relative one against `base`. */
|
|
150
|
-
function streamPath(template: string | undefined, opts: CommandOptions, base: string): string | undefined {
|
|
177
|
+
function streamPath(template: string | undefined, opts: CommandOptions, base: string, templates: TemplateOptions): string | undefined {
|
|
151
178
|
if (!template) return undefined;
|
|
152
|
-
const rendered = opts.context ? renderTemplateString(template, opts.context) : template;
|
|
179
|
+
const rendered = opts.context ? renderTemplateString(template, opts.context, templates) : template;
|
|
153
180
|
return isAbsolute(rendered) ? rendered : resolve(base, rendered);
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -177,8 +204,8 @@ export async function runScriptFunction(
|
|
|
177
204
|
LOGGER.verbose('command: %s', shellQuote(command));
|
|
178
205
|
// stdin is input, like a path option; stdout is an artifact, found like one.
|
|
179
206
|
const workDir = (opts.context?.paths as { work_dir?: string } | undefined)?.work_dir;
|
|
180
|
-
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot);
|
|
181
|
-
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot);
|
|
207
|
+
const stdinPath = streamPath(fn.definition.stdin, opts, opts.configRoot, functionTemplateOptions(fn));
|
|
208
|
+
const stdoutPath = streamPath(fn.definition.stdout, opts, workDir ?? opts.configRoot, functionTemplateOptions(fn));
|
|
182
209
|
// Wire the well-known roles the tool declares: progress onto the record, meta into the run.
|
|
183
210
|
let progress: Record<string, unknown> | undefined;
|
|
184
211
|
const roles: Record<string, RoleOption> = {
|
|
@@ -228,6 +255,6 @@ export function buildResultRecord(p: {
|
|
|
228
255
|
artifact_keys: p.artifactKeys,
|
|
229
256
|
completed_at: p.now,
|
|
230
257
|
config: p.fn.config,
|
|
231
|
-
...renderResultMap(p.fn.definition.result, p.context),
|
|
258
|
+
...renderResultMap(p.fn.definition.result, p.context, functionTemplateOptions(p.fn)),
|
|
232
259
|
};
|
|
233
260
|
}
|
package/src/template.ts
CHANGED
|
@@ -2,46 +2,87 @@
|
|
|
2
2
|
/** Values available to dotted-path template expressions. */
|
|
3
3
|
export type TemplateContext = { [key: string]: unknown };
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
5
|
+
/** How templates treat missing values. */
|
|
6
|
+
export interface TemplateOptions {
|
|
7
|
+
/** Every unknown path is an error, `params` too (default false: a missing `params` field renders `''`, and an option naming one is unset). */
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Caller-supplied fields: one a request leaves out is unset, not a config error.
|
|
12
|
+
const OPTIONAL_ROOTS = ['params'];
|
|
13
|
+
const TEMPLATE = /\$\{\s*([A-Za-z0-9_.-]+)\s*\}|\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
14
|
+
|
|
15
|
+
// The value at a dot path, or MISSING when the path is not there.
|
|
16
|
+
const MISSING = Symbol('missing');
|
|
17
|
+
function resolvePath(context: TemplateContext, expression: string): unknown {
|
|
7
18
|
let current: unknown = context;
|
|
8
19
|
for (const part of String(expression).split('.').filter(Boolean)) {
|
|
9
|
-
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part))
|
|
10
|
-
throw new Error(`unknown template value: ${expression}`);
|
|
11
|
-
}
|
|
20
|
+
if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) return MISSING;
|
|
12
21
|
current = (current as TemplateContext)[part];
|
|
13
22
|
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function optionalRoot(expression: string, opts?: TemplateOptions): boolean {
|
|
27
|
+
return !opts?.strict && OPTIONAL_ROOTS.includes(String(expression).split('.').filter(Boolean)[0] ?? '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A dot-path value from the context; objects become JSON and null is `''`.
|
|
32
|
+
* An unknown path is an error, except a missing `params` field, which is `''`
|
|
33
|
+
* unless `opts.strict`.
|
|
34
|
+
*/
|
|
35
|
+
export function lookupTemplateValue(context: TemplateContext, expression: string, opts?: TemplateOptions): string {
|
|
36
|
+
const current = resolvePath(context, expression);
|
|
37
|
+
if (current === MISSING) {
|
|
38
|
+
if (optionalRoot(expression, opts)) return '';
|
|
39
|
+
throw new Error(`unknown template value: ${expression}`);
|
|
40
|
+
}
|
|
14
41
|
if (current === undefined || current === null) return '';
|
|
15
42
|
return typeof current === 'object' ? JSON.stringify(current) : String(current);
|
|
16
43
|
}
|
|
17
44
|
|
|
45
|
+
/**
|
|
46
|
+
* True when `value` names a `params` field that is missing or null, alone
|
|
47
|
+
* (`${params.x}`) or inside a longer string (`v-${params.x}`), so an option
|
|
48
|
+
* or positional set to it is left unset. Always false with `opts.strict`.
|
|
49
|
+
*/
|
|
50
|
+
export function isUnsetTemplate(value: unknown, context: TemplateContext, opts?: TemplateOptions): boolean {
|
|
51
|
+
if (typeof value !== 'string') return false;
|
|
52
|
+
for (const [, dollar, brace] of value.matchAll(TEMPLATE)) {
|
|
53
|
+
const expression = dollar || brace;
|
|
54
|
+
if (!optionalRoot(expression, opts)) continue;
|
|
55
|
+
const current = resolvePath(context, expression);
|
|
56
|
+
if (current === MISSING || current === undefined || current === null) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
18
61
|
/** Replace dotted-path ${expression} and {{expression}} templates using the context. */
|
|
19
|
-
export function renderTemplateString(value: string, context: TemplateContext): string {
|
|
20
|
-
return String(value).replace(
|
|
21
|
-
lookupTemplateValue(context, dollar || brace),
|
|
22
|
-
);
|
|
62
|
+
export function renderTemplateString(value: string, context: TemplateContext, opts?: TemplateOptions): string {
|
|
63
|
+
return String(value).replace(TEMPLATE, (_, dollar, brace) => lookupTemplateValue(context, dollar || brace, opts));
|
|
23
64
|
}
|
|
24
65
|
|
|
25
66
|
/** Render strings and the strings in arrays; other scalars keep their type. */
|
|
26
|
-
export function renderConfigValue(value: unknown, context?: TemplateContext | null): unknown {
|
|
67
|
+
export function renderConfigValue(value: unknown, context?: TemplateContext | null, opts?: TemplateOptions): unknown {
|
|
27
68
|
if (!context) return value;
|
|
28
|
-
if (typeof value === 'string') return renderTemplateString(value, context);
|
|
29
|
-
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context));
|
|
69
|
+
if (typeof value === 'string') return renderTemplateString(value, context, opts);
|
|
70
|
+
if (Array.isArray(value)) return value.map((item) => renderConfigValue(item, context, opts));
|
|
30
71
|
return value;
|
|
31
72
|
}
|
|
32
73
|
|
|
33
74
|
/** Deep-render a `result` map value: strings, arrays and nested objects. */
|
|
34
|
-
export function renderResultValue(value: unknown, context: TemplateContext): unknown {
|
|
35
|
-
if (typeof value === 'string') return renderTemplateString(value, context);
|
|
36
|
-
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context));
|
|
75
|
+
export function renderResultValue(value: unknown, context: TemplateContext, opts?: TemplateOptions): unknown {
|
|
76
|
+
if (typeof value === 'string') return renderTemplateString(value, context, opts);
|
|
77
|
+
if (Array.isArray(value)) return value.map((item) => renderResultValue(item, context, opts));
|
|
37
78
|
if (value && typeof value === 'object') {
|
|
38
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context)]));
|
|
79
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, renderResultValue(v, context, opts)]));
|
|
39
80
|
}
|
|
40
81
|
return value;
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
/** Render a function's optional `result` map into extra result fields. */
|
|
44
|
-
export function renderResultMap(resultMap: unknown, context: TemplateContext): { [key: string]: unknown } {
|
|
85
|
+
export function renderResultMap(resultMap: unknown, context: TemplateContext, opts?: TemplateOptions): { [key: string]: unknown } {
|
|
45
86
|
if (!resultMap || typeof resultMap !== 'object' || Array.isArray(resultMap)) return {};
|
|
46
|
-
return renderResultValue(resultMap, context) as { [key: string]: unknown };
|
|
87
|
+
return renderResultValue(resultMap, context, opts) as { [key: string]: unknown };
|
|
47
88
|
}
|