@secureci/cli 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,676 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createContext = createContext;
4
+ exports.resolveExpressionValue = resolveExpressionValue;
5
+ exports.substituteExpressions = substituteExpressions;
6
+ exports.substituteRecord = substituteRecord;
7
+ exports.withImplicitSuccess = withImplicitSuccess;
8
+ exports.evaluateCondition = evaluateCondition;
9
+ exports.ifEvaluationError = ifEvaluationError;
10
+ exports.validateExpression = validateExpression;
11
+ const logger_1 = require("../utils/logger");
12
+ const hash_files_1 = require("./hash-files");
13
+ function createContext(env = {}, inputs = {}, secrets = {}, github = {}, matrix = {}) {
14
+ return {
15
+ env,
16
+ inputs,
17
+ secrets,
18
+ steps: {},
19
+ github,
20
+ matrix,
21
+ needs: {},
22
+ job: {},
23
+ runner: {},
24
+ vars: {},
25
+ strategy: {},
26
+ jobFailed: false
27
+ };
28
+ }
29
+ const OPERATORS = ['==', '!=', '<=', '>=', '&&', '||', '<', '>', '!'];
30
+ function tokenize(input) {
31
+ const tokens = [];
32
+ let i = 0;
33
+ while (i < input.length) {
34
+ const ch = input[i];
35
+ if (/\s/.test(ch)) {
36
+ i++;
37
+ continue;
38
+ }
39
+ const single = {
40
+ '(': { type: 'lparen' },
41
+ ')': { type: 'rparen' },
42
+ '[': { type: 'lbracket' },
43
+ ']': { type: 'rbracket' },
44
+ ',': { type: 'comma' },
45
+ '*': { type: 'star' },
46
+ '.': { type: 'dot' }
47
+ };
48
+ if (single[ch] && !(ch === '.' && /[0-9]/.test(input[i + 1] ?? ''))) {
49
+ tokens.push(single[ch]);
50
+ i++;
51
+ continue;
52
+ }
53
+ if (ch === "'" || ch === '"') {
54
+ // GitHub strings are single-quoted, with '' as an escaped quote.
55
+ const quote = ch;
56
+ let j = i + 1;
57
+ let value = '';
58
+ while (j < input.length) {
59
+ if (input[j] === quote) {
60
+ if (input[j + 1] === quote) {
61
+ value += quote;
62
+ j += 2;
63
+ continue;
64
+ }
65
+ break;
66
+ }
67
+ value += input[j];
68
+ j++;
69
+ }
70
+ if (j >= input.length) {
71
+ throw new Error('Unterminated string literal');
72
+ }
73
+ tokens.push({ type: 'string', value });
74
+ i = j + 1;
75
+ continue;
76
+ }
77
+ const op = OPERATORS.find((candidate) => input.startsWith(candidate, i));
78
+ if (op) {
79
+ tokens.push({ type: 'op', value: op });
80
+ i += op.length;
81
+ continue;
82
+ }
83
+ if (/[0-9.]/.test(ch) || (ch === '-' && /[0-9.]/.test(input[i + 1]))) {
84
+ const match = input
85
+ .slice(i)
86
+ .match(/^-?(?:0x[0-9a-fA-F]+|[0-9]*\.?[0-9]+(?:e[+-]?[0-9]+)?)/);
87
+ if (!match)
88
+ throw new Error(`Invalid number at position ${i}`);
89
+ tokens.push({ type: 'number', value: Number(match[0]) });
90
+ i += match[0].length;
91
+ continue;
92
+ }
93
+ if (/[A-Za-z_]/.test(ch)) {
94
+ let j = i;
95
+ while (j < input.length && /[A-Za-z0-9_-]/.test(input[j]))
96
+ j++;
97
+ tokens.push({ type: 'ident', value: input.slice(i, j) });
98
+ i = j;
99
+ continue;
100
+ }
101
+ throw new Error(`Unexpected character '${ch}' at position ${i}`);
102
+ }
103
+ tokens.push({ type: 'eof' });
104
+ return tokens;
105
+ }
106
+ /** GitHub truthiness: false, 0, -0, '', null and NaN are falsy. */
107
+ function toBoolean(value) {
108
+ if (value === undefined || value === null)
109
+ return false;
110
+ if (typeof value === 'boolean')
111
+ return value;
112
+ if (typeof value === 'number')
113
+ return value !== 0 && !Number.isNaN(value);
114
+ if (typeof value === 'string')
115
+ return value !== '';
116
+ return true;
117
+ }
118
+ /** GitHub's number coercion for mixed-type comparisons. */
119
+ function toNumber(value) {
120
+ if (value === undefined || value === null)
121
+ return 0;
122
+ if (typeof value === 'boolean')
123
+ return value ? 1 : 0;
124
+ if (typeof value === 'number')
125
+ return value;
126
+ if (typeof value === 'string') {
127
+ const trimmed = value.trim();
128
+ return trimmed === '' ? 0 : Number(trimmed);
129
+ }
130
+ return NaN;
131
+ }
132
+ function isPrimitive(value) {
133
+ return value === null || value === undefined || typeof value !== 'object';
134
+ }
135
+ function looseEquals(a, b) {
136
+ if (typeof a === 'string' && typeof b === 'string') {
137
+ return a.toLowerCase() === b.toLowerCase();
138
+ }
139
+ if (!isPrimitive(a) || !isPrimitive(b))
140
+ return a === b;
141
+ if (typeof a === typeof b && typeof a !== 'string')
142
+ return a === b;
143
+ return toNumber(a) === toNumber(b);
144
+ }
145
+ function compare(a, b) {
146
+ if (typeof a === 'string' && typeof b === 'string') {
147
+ const x = a.toLowerCase();
148
+ const y = b.toLowerCase();
149
+ return x < y ? -1 : x > y ? 1 : 0;
150
+ }
151
+ const x = toNumber(a);
152
+ const y = toNumber(b);
153
+ if (Number.isNaN(x) || Number.isNaN(y))
154
+ return NaN;
155
+ return x < y ? -1 : x > y ? 1 : 0;
156
+ }
157
+ /** How a value is rendered when substituted into text. */
158
+ function toText(value) {
159
+ if (value === undefined || value === null)
160
+ return '';
161
+ if (Array.isArray(value))
162
+ return 'Array';
163
+ if (typeof value === 'object')
164
+ return 'Object';
165
+ return String(value);
166
+ }
167
+ /** Wraps the result of a `*` filter so further access maps over it. */
168
+ class FilteredArray {
169
+ constructor(items) {
170
+ this.items = items;
171
+ }
172
+ }
173
+ function unwrap(value) {
174
+ return value instanceof FilteredArray ? value.items : value;
175
+ }
176
+ function member(target, key) {
177
+ if (target instanceof FilteredArray) {
178
+ return new FilteredArray(target.items.flatMap((item) => {
179
+ const v = member(item, key);
180
+ return v === undefined ? [] : [unwrap(v)];
181
+ }));
182
+ }
183
+ if (target === null || target === undefined)
184
+ return undefined;
185
+ if (Array.isArray(target)) {
186
+ return typeof key === 'number' ? target[key] : undefined;
187
+ }
188
+ if (typeof target === 'object') {
189
+ const record = target;
190
+ if (Object.prototype.hasOwnProperty.call(record, key)) {
191
+ return record[key];
192
+ }
193
+ // Property names are case-insensitive in GitHub expressions.
194
+ const match = Object.keys(record).find((k) => k.toLowerCase() === String(key).toLowerCase());
195
+ return match === undefined ? undefined : record[match];
196
+ }
197
+ return undefined;
198
+ }
199
+ function star(target) {
200
+ const source = unwrap(target);
201
+ if (Array.isArray(source))
202
+ return new FilteredArray([...source]);
203
+ if (source && typeof source === 'object') {
204
+ return new FilteredArray(Object.values(source));
205
+ }
206
+ return new FilteredArray([]);
207
+ }
208
+ function stepsContext(ctx) {
209
+ const steps = {};
210
+ for (const [id, info] of Object.entries(ctx.steps)) {
211
+ steps[id] = {
212
+ outcome: info.outcome,
213
+ conclusion: info.conclusion ?? info.outcome,
214
+ outputs: info.outputs
215
+ };
216
+ }
217
+ return steps;
218
+ }
219
+ function rootContext(name, ctx) {
220
+ switch (name) {
221
+ case 'env':
222
+ return ctx.env;
223
+ case 'inputs':
224
+ return ctx.inputs;
225
+ case 'secrets':
226
+ if (ctx.secretsUnavailable) {
227
+ throw new Error("Unrecognized named-value: 'secrets' (composite actions get secrets as inputs)");
228
+ }
229
+ return ctx.secrets;
230
+ case 'steps':
231
+ return stepsContext(ctx);
232
+ case 'github':
233
+ return ctx.github;
234
+ case 'matrix':
235
+ return ctx.matrix;
236
+ case 'needs':
237
+ return ctx.needs;
238
+ case 'job':
239
+ return ctx.job;
240
+ case 'runner':
241
+ return ctx.runner;
242
+ case 'vars':
243
+ return ctx.vars;
244
+ case 'strategy':
245
+ return ctx.strategy;
246
+ case 'jobs':
247
+ if (ctx.jobs)
248
+ return ctx.jobs;
249
+ throw new Error(`Unrecognized named-value: 'jobs'`);
250
+ default:
251
+ throw new Error(`Unrecognized named-value: '${name}'`);
252
+ }
253
+ }
254
+ function formatString(template, args) {
255
+ return template.replace(/\{\{|\}\}|\{(\d+)\}/g, (match, index) => {
256
+ if (match === '{{')
257
+ return '{';
258
+ if (match === '}}')
259
+ return '}';
260
+ const i = Number(index);
261
+ if (i >= args.length) {
262
+ throw new Error(`format(): no argument for {${i}}`);
263
+ }
264
+ return toText(args[i]);
265
+ });
266
+ }
267
+ /** Functions and their [min, max] number of arguments. */
268
+ const FUNCTIONS = {
269
+ success: [0, 0],
270
+ failure: [0, 0],
271
+ always: [0, 0],
272
+ cancelled: [0, 0],
273
+ contains: [2, 2],
274
+ startswith: [2, 2],
275
+ endswith: [2, 2],
276
+ format: [1, Infinity],
277
+ join: [1, 2],
278
+ tojson: [1, 1],
279
+ fromjson: [1, 1],
280
+ hashfiles: [1, Infinity]
281
+ };
282
+ /** Check a function name and its argument count, as GitHub does. */
283
+ function checkCall(name, argCount, allowed) {
284
+ const arity = FUNCTIONS[name.toLowerCase()];
285
+ if (!arity || (allowed && !allowed.has(name.toLowerCase()))) {
286
+ throw new Error(`Unrecognized function: '${name}'`);
287
+ }
288
+ if (argCount < arity[0]) {
289
+ throw new Error(`Too few parameters supplied: '${name}'`);
290
+ }
291
+ if (argCount > arity[1]) {
292
+ throw new Error(`Too many parameters supplied: '${name}'`);
293
+ }
294
+ }
295
+ function callFunction(name, rawArgs, ctx) {
296
+ checkCall(name, rawArgs.length);
297
+ const args = rawArgs.map(unwrap);
298
+ switch (name.toLowerCase()) {
299
+ case 'success':
300
+ return !ctx.jobFailed && (ctx.dependencies?.allSucceeded ?? true);
301
+ case 'failure':
302
+ return ctx.jobFailed || (ctx.dependencies?.anyFailed ?? false);
303
+ case 'always':
304
+ return true;
305
+ case 'cancelled':
306
+ return false;
307
+ case 'contains': {
308
+ const [search, item] = args;
309
+ if (Array.isArray(search)) {
310
+ return search.some((element) => looseEquals(element, item));
311
+ }
312
+ return toText(search).toLowerCase().includes(toText(item).toLowerCase());
313
+ }
314
+ case 'startswith':
315
+ return toText(args[0])
316
+ .toLowerCase()
317
+ .startsWith(toText(args[1]).toLowerCase());
318
+ case 'endswith':
319
+ return toText(args[0])
320
+ .toLowerCase()
321
+ .endsWith(toText(args[1]).toLowerCase());
322
+ case 'format':
323
+ return formatString(toText(args[0]), args.slice(1));
324
+ case 'join': {
325
+ const separator = args.length > 1 ? toText(args[1]) : ',';
326
+ return Array.isArray(args[0])
327
+ ? args[0].map(toText).join(separator)
328
+ : toText(args[0]);
329
+ }
330
+ case 'tojson':
331
+ return JSON.stringify(args[0] ?? null, null, 2);
332
+ case 'fromjson':
333
+ return JSON.parse(toText(args[0]));
334
+ case 'hashfiles': {
335
+ const workspace = typeof ctx.github.workspace === 'string' && ctx.github.workspace
336
+ ? ctx.github.workspace
337
+ : process.cwd();
338
+ return (0, hash_files_1.hashFiles)(workspace, args.map(toText));
339
+ }
340
+ default:
341
+ throw new Error(`Unrecognized function: '${name}'`);
342
+ }
343
+ }
344
+ class Parser {
345
+ /** With a scope, only check names and syntax; nothing is evaluated. */
346
+ constructor(tokens, ctx, scope) {
347
+ this.tokens = tokens;
348
+ this.ctx = ctx;
349
+ this.scope = scope;
350
+ this.pos = 0;
351
+ }
352
+ peek() {
353
+ return this.tokens[this.pos];
354
+ }
355
+ next() {
356
+ return this.tokens[this.pos++];
357
+ }
358
+ expect(type) {
359
+ if (this.peek().type !== type) {
360
+ throw new Error(`Expected ${type} but got ${this.peek().type}`);
361
+ }
362
+ this.next();
363
+ }
364
+ isOp(...values) {
365
+ const tok = this.peek();
366
+ return tok.type === 'op' && values.includes(tok.value);
367
+ }
368
+ parse() {
369
+ const value = this.parseOr();
370
+ if (this.peek().type !== 'eof') {
371
+ throw new Error(`Unexpected token: ${this.peek().type}`);
372
+ }
373
+ return unwrap(value);
374
+ }
375
+ // `||` / `&&` return the deciding operand, like GitHub (and JavaScript).
376
+ parseOr() {
377
+ let left = this.parseAnd();
378
+ while (this.isOp('||')) {
379
+ this.next();
380
+ const right = this.parseAnd();
381
+ left = toBoolean(unwrap(left)) ? left : right;
382
+ }
383
+ return left;
384
+ }
385
+ parseAnd() {
386
+ let left = this.parseEquality();
387
+ while (this.isOp('&&')) {
388
+ this.next();
389
+ const right = this.parseEquality();
390
+ left = toBoolean(unwrap(left)) ? right : left;
391
+ }
392
+ return left;
393
+ }
394
+ parseEquality() {
395
+ let left = this.parseComparison();
396
+ while (this.isOp('==', '!=')) {
397
+ const op = this.next().value;
398
+ const right = this.parseComparison();
399
+ const eq = looseEquals(unwrap(left), unwrap(right));
400
+ left = op === '==' ? eq : !eq;
401
+ }
402
+ return left;
403
+ }
404
+ parseComparison() {
405
+ let left = this.parseUnary();
406
+ while (this.isOp('<', '<=', '>', '>=')) {
407
+ const op = this.next().value;
408
+ const right = this.parseUnary();
409
+ const c = compare(unwrap(left), unwrap(right));
410
+ left =
411
+ !Number.isNaN(c) &&
412
+ (op === '<'
413
+ ? c < 0
414
+ : op === '<='
415
+ ? c <= 0
416
+ : op === '>'
417
+ ? c > 0
418
+ : c >= 0);
419
+ }
420
+ return left;
421
+ }
422
+ parseUnary() {
423
+ if (this.isOp('!')) {
424
+ this.next();
425
+ return !toBoolean(unwrap(this.parseUnary()));
426
+ }
427
+ return this.parsePostfix(this.parsePrimary());
428
+ }
429
+ parsePostfix(value) {
430
+ for (;;) {
431
+ const tok = this.peek();
432
+ if (tok.type === 'dot') {
433
+ this.next();
434
+ const part = this.next();
435
+ if (part.type === 'star') {
436
+ value = star(value);
437
+ }
438
+ else if (part.type === 'ident') {
439
+ value = member(value, part.value);
440
+ }
441
+ else {
442
+ throw new Error('Expected property name after .');
443
+ }
444
+ }
445
+ else if (tok.type === 'lbracket') {
446
+ this.next();
447
+ if (this.peek().type === 'star') {
448
+ this.next();
449
+ value = star(value);
450
+ }
451
+ else {
452
+ const key = unwrap(this.parseOr());
453
+ value = member(value, typeof key === 'number' ? key : toText(key));
454
+ }
455
+ this.expect('rbracket');
456
+ }
457
+ else {
458
+ return value;
459
+ }
460
+ }
461
+ }
462
+ parsePrimary() {
463
+ const tok = this.next();
464
+ switch (tok.type) {
465
+ case 'lparen': {
466
+ const value = this.parseOr();
467
+ this.expect('rparen');
468
+ return value;
469
+ }
470
+ case 'string':
471
+ case 'number':
472
+ return tok.value;
473
+ case 'ident': {
474
+ const lower = tok.value.toLowerCase();
475
+ if (lower === 'true')
476
+ return true;
477
+ if (lower === 'false')
478
+ return false;
479
+ if (lower === 'null')
480
+ return null;
481
+ if (this.peek().type === 'lparen') {
482
+ this.next();
483
+ const args = [];
484
+ if (this.peek().type !== 'rparen') {
485
+ args.push(this.parseOr());
486
+ while (this.peek().type === 'comma') {
487
+ this.next();
488
+ args.push(this.parseOr());
489
+ }
490
+ }
491
+ this.expect('rparen');
492
+ if (this.scope) {
493
+ checkCall(tok.value, args.length, this.scope.functions);
494
+ return undefined;
495
+ }
496
+ return callFunction(tok.value, args, this.ctx);
497
+ }
498
+ if (this.scope) {
499
+ if (!this.scope.contexts.has(lower)) {
500
+ throw new Error(`Unrecognized named-value: '${tok.value}'`);
501
+ }
502
+ return undefined;
503
+ }
504
+ return rootContext(lower, this.ctx);
505
+ }
506
+ default:
507
+ throw new Error(`Unexpected token: ${tok.type}`);
508
+ }
509
+ }
510
+ }
511
+ function evaluate(expr, ctx) {
512
+ return new Parser(tokenize(expr), ctx).parse();
513
+ }
514
+ /**
515
+ * Resolve a single `${{ expr }}` expression to its underlying value (not
516
+ * coerced to boolean), used for e.g. action output value mapping such as
517
+ * `outputs.greeting.value: ${{ steps.greet.outputs.greeting }}`.
518
+ * Returns undefined if the string isn't a (sole) `${{ }}` expression or
519
+ * fails to evaluate.
520
+ */
521
+ function resolveExpressionValue(expr, ctx) {
522
+ if (expr === undefined)
523
+ return undefined;
524
+ const trimmed = expr.trim();
525
+ const wrapped = trimmed.match(/^\$\{\{([\s\S]*)\}\}$/);
526
+ const inner = wrapped ? wrapped[1].trim() : trimmed;
527
+ try {
528
+ return evaluate(inner, ctx);
529
+ }
530
+ catch (error) {
531
+ logger_1.logger.warn(`Failed to resolve expression "${expr}": ${error instanceof Error ? error.message : 'Unknown error'}.`);
532
+ return undefined;
533
+ }
534
+ }
535
+ /**
536
+ * Replace every `${{ expr }}` occurrence within a string with its resolved
537
+ * value. Used for substituting env/inputs/secrets/steps/github/matrix
538
+ * references inside `run:` command text. Expressions that fail to resolve
539
+ * are left as `undefined` -> empty string, matching GitHub Actions behavior
540
+ * for unresolved references.
541
+ */
542
+ function substituteExpressions(input, ctx) {
543
+ return input.replace(/\$\{\{([\s\S]*?)\}\}/g, (full, inner) => {
544
+ try {
545
+ return toText(evaluate(inner.trim(), ctx));
546
+ }
547
+ catch (error) {
548
+ logger_1.logger.warn(`Failed to resolve expression "${full}": ${error instanceof Error ? error.message : 'Unknown error'}.`);
549
+ return full;
550
+ }
551
+ });
552
+ }
553
+ /**
554
+ * Resolve every value of an `env:` / `with:` map against the given context,
555
+ * stringifying non-string YAML scalars (e.g. `PORT: 3000`) the way the
556
+ * runner does before exporting them.
557
+ */
558
+ function substituteRecord(record, ctx) {
559
+ const result = {};
560
+ for (const [key, value] of Object.entries(record || {})) {
561
+ result[key] = substituteExpressions(String(value ?? ''), ctx);
562
+ }
563
+ return result;
564
+ }
565
+ const STATUS_FUNCTION = /\b(success|failure|always|cancelled)\s*\(/i;
566
+ /**
567
+ * GitHub wraps a job or step `if:` that doesn't call a status function in
568
+ * an implicit `success() && (...)`; an absent `if:` means `success()`.
569
+ */
570
+ function withImplicitSuccess(expr) {
571
+ if (expr === undefined || expr === null || String(expr).trim() === '') {
572
+ return 'success()';
573
+ }
574
+ let inner = String(expr).trim();
575
+ const wrapped = inner.match(/^\$\{\{([\s\S]*)\}\}$/);
576
+ if (wrapped)
577
+ inner = wrapped[1].trim();
578
+ return STATUS_FUNCTION.test(inner) ? inner : `success() && (${inner})`;
579
+ }
580
+ /**
581
+ * Evaluate an `if:` condition string against the given context.
582
+ * Returns true when the condition is undefined/empty, or when it fails to
583
+ * parse (fails open, with a warning, to avoid unexpectedly skipping steps).
584
+ */
585
+ function evaluateCondition(expr, ctx) {
586
+ if (expr === undefined)
587
+ return true;
588
+ let trimmed = String(expr).trim();
589
+ const wrapped = trimmed.match(/^\$\{\{([\s\S]*)\}\}$/);
590
+ if (wrapped)
591
+ trimmed = wrapped[1].trim();
592
+ if (trimmed === '')
593
+ return true;
594
+ // An expression that can't be evaluated is an error, as on GitHub (where
595
+ // it fails the step or job), never a condition that passes.
596
+ return toBoolean(evaluate(trimmed, ctx));
597
+ }
598
+ /** The error for an `if:` that couldn't be evaluated, worded like GitHub's. */
599
+ function ifEvaluationError(target, error) {
600
+ return `Error when evaluating 'if' for ${target}. ${error instanceof Error ? error.message : String(error)}`;
601
+ }
602
+ const STATUS_FUNCTIONS = ['success', 'failure', 'always', 'cancelled'];
603
+ const GENERAL_FUNCTIONS = [
604
+ 'contains',
605
+ 'startswith',
606
+ 'endswith',
607
+ 'format',
608
+ 'join',
609
+ 'tojson',
610
+ 'fromjson'
611
+ ];
612
+ /**
613
+ * GitHub's context availability for `if:`
614
+ * (https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability).
615
+ */
616
+ const SCOPES = {
617
+ 'job-if': {
618
+ contexts: new Set(['github', 'needs', 'vars', 'inputs']),
619
+ functions: new Set([...STATUS_FUNCTIONS, ...GENERAL_FUNCTIONS])
620
+ },
621
+ 'step-if': {
622
+ contexts: new Set([
623
+ 'github',
624
+ 'needs',
625
+ 'strategy',
626
+ 'matrix',
627
+ 'job',
628
+ 'runner',
629
+ 'env',
630
+ 'vars',
631
+ 'secrets',
632
+ 'steps',
633
+ 'inputs'
634
+ ]),
635
+ functions: new Set([...STATUS_FUNCTIONS, ...GENERAL_FUNCTIONS, 'hashfiles'])
636
+ },
637
+ 'composite-step-if': {
638
+ contexts: new Set([
639
+ 'github',
640
+ 'needs',
641
+ 'strategy',
642
+ 'matrix',
643
+ 'job',
644
+ 'runner',
645
+ 'env',
646
+ 'vars',
647
+ 'steps',
648
+ 'inputs'
649
+ ]),
650
+ functions: new Set([...STATUS_FUNCTIONS, ...GENERAL_FUNCTIONS, 'hashfiles'])
651
+ }
652
+ };
653
+ /**
654
+ * Check an `if:` expression without evaluating it: syntax, and that it only
655
+ * uses contexts and functions available where it appears. Returns the error
656
+ * message, or undefined if it's valid. GitHub rejects a workflow with such
657
+ * an error before running any job.
658
+ */
659
+ function validateExpression(expr, location) {
660
+ if (expr === undefined || typeof expr === 'boolean')
661
+ return undefined;
662
+ let trimmed = expr.trim();
663
+ const wrapped = trimmed.match(/^\$\{\{([\s\S]*)\}\}$/);
664
+ if (wrapped)
665
+ trimmed = wrapped[1].trim();
666
+ if (trimmed === '')
667
+ return undefined;
668
+ try {
669
+ new Parser(tokenize(trimmed), createContext(), SCOPES[location]).parse();
670
+ return undefined;
671
+ }
672
+ catch (error) {
673
+ return error instanceof Error ? error.message : String(error);
674
+ }
675
+ }
676
+ //# sourceMappingURL=expression.js.map