@magland/mochi 0.3.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.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
package/dist/ci/expr.js
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The GitHub Actions expression language: the `${{ ... }}` fragments in
|
|
3
|
+
// workflow files. One evaluator serves both sides of the CI system: the
|
|
4
|
+
// server evaluates job-level conditions, concurrency groups, and matrix
|
|
5
|
+
// substitutions, and the runner evaluates step-level conditions, env maps,
|
|
6
|
+
// and run bodies. Both are this module, so behavior cannot drift.
|
|
7
|
+
//
|
|
8
|
+
// The language is small: literals, `!`, comparisons, `&&`/`||`, property and
|
|
9
|
+
// index dereference on contexts, and a fixed set of functions. `&&` and `||`
|
|
10
|
+
// return operand values as in JavaScript. Comparisons follow GitHub's rules:
|
|
11
|
+
// equal-typed strings compare case-insensitively, mixed types are cast to
|
|
12
|
+
// numbers. The object-filter syntax (`*` in a dereference chain) is not
|
|
13
|
+
// implemented; nothing in scope uses it and it fails with a clear message.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.ExprError = void 0;
|
|
16
|
+
exports.truthy = truthy;
|
|
17
|
+
exports.stringify = stringify;
|
|
18
|
+
exports.evaluate = evaluate;
|
|
19
|
+
exports.render = render;
|
|
20
|
+
exports.renderDeep = renderDeep;
|
|
21
|
+
exports.evalCondition = evalCondition;
|
|
22
|
+
exports.mentionsStatusOverride = mentionsStatusOverride;
|
|
23
|
+
class ExprError extends Error {
|
|
24
|
+
}
|
|
25
|
+
exports.ExprError = ExprError;
|
|
26
|
+
function tokenize(src) {
|
|
27
|
+
const tokens = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < src.length) {
|
|
30
|
+
const c = src[i];
|
|
31
|
+
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (c === "'") {
|
|
36
|
+
// Single-quoted string; '' is an escaped quote.
|
|
37
|
+
let s = '';
|
|
38
|
+
i++;
|
|
39
|
+
for (;;) {
|
|
40
|
+
if (i >= src.length)
|
|
41
|
+
throw new ExprError('unterminated string literal');
|
|
42
|
+
if (src[i] === "'") {
|
|
43
|
+
if (src[i + 1] === "'") {
|
|
44
|
+
s += "'";
|
|
45
|
+
i += 2;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
i++;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
s += src[i++];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
tokens.push({ kind: 'str', value: s });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const two = src.slice(i, i + 2);
|
|
60
|
+
if (two === '==' || two === '!=' || two === '<=' || two === '>=' || two === '&&' || two === '||') {
|
|
61
|
+
tokens.push({ kind: 'op', value: two });
|
|
62
|
+
i += 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if ('!<>()[].,'.includes(c)) {
|
|
66
|
+
tokens.push({ kind: 'op', value: c });
|
|
67
|
+
i++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (/[0-9]/.test(c) || (c === '-' && /[0-9.]/.test(src[i + 1] ?? ''))) {
|
|
71
|
+
const m = src.slice(i).match(/^-?(0x[0-9a-fA-F]+|\d+(\.\d+)?([eE][+-]?\d+)?|\.\d+)/);
|
|
72
|
+
if (!m)
|
|
73
|
+
throw new ExprError(`invalid number at: ${src.slice(i, i + 12)}`);
|
|
74
|
+
tokens.push({ kind: 'num', value: Number(m[0]) });
|
|
75
|
+
i += m[0].length;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
79
|
+
const m = src.slice(i).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
|
|
80
|
+
tokens.push({ kind: 'ident', value: m[0] });
|
|
81
|
+
i += m[0].length;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (c === '*') {
|
|
85
|
+
throw new ExprError('the object-filter syntax (*) is not supported');
|
|
86
|
+
}
|
|
87
|
+
throw new ExprError(`unexpected character '${c}' in expression`);
|
|
88
|
+
}
|
|
89
|
+
return tokens;
|
|
90
|
+
}
|
|
91
|
+
class Parser {
|
|
92
|
+
tokens;
|
|
93
|
+
pos = 0;
|
|
94
|
+
constructor(tokens) {
|
|
95
|
+
this.tokens = tokens;
|
|
96
|
+
}
|
|
97
|
+
parse() {
|
|
98
|
+
const n = this.or();
|
|
99
|
+
if (this.pos < this.tokens.length) {
|
|
100
|
+
throw new ExprError(`unexpected token after expression: ${JSON.stringify(this.tokens[this.pos])}`);
|
|
101
|
+
}
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
peekOp(v) {
|
|
105
|
+
const t = this.tokens[this.pos];
|
|
106
|
+
return t !== undefined && t.kind === 'op' && t.value === v;
|
|
107
|
+
}
|
|
108
|
+
eatOp(v) {
|
|
109
|
+
if (this.peekOp(v)) {
|
|
110
|
+
this.pos++;
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
expectOp(v) {
|
|
116
|
+
if (!this.eatOp(v))
|
|
117
|
+
throw new ExprError(`expected '${v}' in expression`);
|
|
118
|
+
}
|
|
119
|
+
or() {
|
|
120
|
+
let l = this.and();
|
|
121
|
+
while (this.eatOp('||'))
|
|
122
|
+
l = { t: 'bin', op: '||', l, r: this.and() };
|
|
123
|
+
return l;
|
|
124
|
+
}
|
|
125
|
+
and() {
|
|
126
|
+
let l = this.equality();
|
|
127
|
+
while (this.eatOp('&&'))
|
|
128
|
+
l = { t: 'bin', op: '&&', l, r: this.equality() };
|
|
129
|
+
return l;
|
|
130
|
+
}
|
|
131
|
+
equality() {
|
|
132
|
+
let l = this.comparison();
|
|
133
|
+
for (;;) {
|
|
134
|
+
if (this.eatOp('=='))
|
|
135
|
+
l = { t: 'bin', op: '==', l, r: this.comparison() };
|
|
136
|
+
else if (this.eatOp('!='))
|
|
137
|
+
l = { t: 'bin', op: '!=', l, r: this.comparison() };
|
|
138
|
+
else
|
|
139
|
+
return l;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
comparison() {
|
|
143
|
+
let l = this.unary();
|
|
144
|
+
for (;;) {
|
|
145
|
+
if (this.eatOp('<='))
|
|
146
|
+
l = { t: 'bin', op: '<=', l, r: this.unary() };
|
|
147
|
+
else if (this.eatOp('>='))
|
|
148
|
+
l = { t: 'bin', op: '>=', l, r: this.unary() };
|
|
149
|
+
else if (this.eatOp('<'))
|
|
150
|
+
l = { t: 'bin', op: '<', l, r: this.unary() };
|
|
151
|
+
else if (this.eatOp('>'))
|
|
152
|
+
l = { t: 'bin', op: '>', l, r: this.unary() };
|
|
153
|
+
else
|
|
154
|
+
return l;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
unary() {
|
|
158
|
+
if (this.eatOp('!'))
|
|
159
|
+
return { t: 'not', e: this.unary() };
|
|
160
|
+
return this.postfix();
|
|
161
|
+
}
|
|
162
|
+
postfix() {
|
|
163
|
+
let n = this.primary();
|
|
164
|
+
for (;;) {
|
|
165
|
+
if (this.eatOp('.')) {
|
|
166
|
+
const t = this.tokens[this.pos];
|
|
167
|
+
if (!t || t.kind !== 'ident')
|
|
168
|
+
throw new ExprError("expected a property name after '.'");
|
|
169
|
+
this.pos++;
|
|
170
|
+
n = { t: 'deref', e: n, prop: t.value };
|
|
171
|
+
}
|
|
172
|
+
else if (this.eatOp('[')) {
|
|
173
|
+
const i = this.or();
|
|
174
|
+
this.expectOp(']');
|
|
175
|
+
n = { t: 'index', e: n, i };
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return n;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
primary() {
|
|
183
|
+
const t = this.tokens[this.pos];
|
|
184
|
+
if (!t)
|
|
185
|
+
throw new ExprError('unexpected end of expression');
|
|
186
|
+
if (t.kind === 'num') {
|
|
187
|
+
this.pos++;
|
|
188
|
+
return { t: 'lit', v: t.value };
|
|
189
|
+
}
|
|
190
|
+
if (t.kind === 'str') {
|
|
191
|
+
this.pos++;
|
|
192
|
+
return { t: 'lit', v: t.value };
|
|
193
|
+
}
|
|
194
|
+
if (t.kind === 'op' && t.value === '(') {
|
|
195
|
+
this.pos++;
|
|
196
|
+
const n = this.or();
|
|
197
|
+
this.expectOp(')');
|
|
198
|
+
return n;
|
|
199
|
+
}
|
|
200
|
+
if (t.kind === 'ident') {
|
|
201
|
+
this.pos++;
|
|
202
|
+
const lower = t.value.toLowerCase();
|
|
203
|
+
if (lower === 'null')
|
|
204
|
+
return { t: 'lit', v: null };
|
|
205
|
+
if (lower === 'true')
|
|
206
|
+
return { t: 'lit', v: true };
|
|
207
|
+
if (lower === 'false')
|
|
208
|
+
return { t: 'lit', v: false };
|
|
209
|
+
if (this.eatOp('(')) {
|
|
210
|
+
const args = [];
|
|
211
|
+
if (!this.peekOp(')')) {
|
|
212
|
+
do {
|
|
213
|
+
args.push(this.or());
|
|
214
|
+
} while (this.eatOp(','));
|
|
215
|
+
}
|
|
216
|
+
this.expectOp(')');
|
|
217
|
+
return { t: 'call', name: lower, args };
|
|
218
|
+
}
|
|
219
|
+
return { t: 'ctx', name: t.value };
|
|
220
|
+
}
|
|
221
|
+
throw new ExprError(`unexpected token in expression: ${JSON.stringify(t)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ---- evaluation ----
|
|
225
|
+
function truthy(v) {
|
|
226
|
+
if (v === null || v === false)
|
|
227
|
+
return false;
|
|
228
|
+
if (typeof v === 'number')
|
|
229
|
+
return v !== 0 && !Number.isNaN(v);
|
|
230
|
+
if (typeof v === 'string')
|
|
231
|
+
return v !== '';
|
|
232
|
+
return true; // arrays and objects
|
|
233
|
+
}
|
|
234
|
+
function toNumber(v) {
|
|
235
|
+
if (v === null)
|
|
236
|
+
return 0;
|
|
237
|
+
if (typeof v === 'boolean')
|
|
238
|
+
return v ? 1 : 0;
|
|
239
|
+
if (typeof v === 'number')
|
|
240
|
+
return v;
|
|
241
|
+
if (typeof v === 'string') {
|
|
242
|
+
const s = v.trim();
|
|
243
|
+
if (s === '')
|
|
244
|
+
return 0;
|
|
245
|
+
return Number(s);
|
|
246
|
+
}
|
|
247
|
+
return NaN;
|
|
248
|
+
}
|
|
249
|
+
function compareEqual(l, r) {
|
|
250
|
+
if (typeof l === 'string' && typeof r === 'string')
|
|
251
|
+
return l.toLowerCase() === r.toLowerCase();
|
|
252
|
+
if (typeof l === typeof r && (l === null) === (r === null)) {
|
|
253
|
+
if (typeof l === 'number') {
|
|
254
|
+
return !Number.isNaN(l) && !Number.isNaN(r) && l === r;
|
|
255
|
+
}
|
|
256
|
+
if (typeof l === 'boolean' || l === null)
|
|
257
|
+
return l === r;
|
|
258
|
+
// Arrays and objects: equal only when the same instance, per GitHub.
|
|
259
|
+
if (typeof l === 'object')
|
|
260
|
+
return l === r;
|
|
261
|
+
}
|
|
262
|
+
const ln = toNumber(l);
|
|
263
|
+
const rn = toNumber(r);
|
|
264
|
+
return !Number.isNaN(ln) && !Number.isNaN(rn) && ln === rn;
|
|
265
|
+
}
|
|
266
|
+
function compareOrder(op, l, r) {
|
|
267
|
+
if (typeof l === 'string' && typeof r === 'string') {
|
|
268
|
+
const a = l.toLowerCase();
|
|
269
|
+
const b = r.toLowerCase();
|
|
270
|
+
if (op === '<')
|
|
271
|
+
return a < b;
|
|
272
|
+
if (op === '<=')
|
|
273
|
+
return a <= b;
|
|
274
|
+
if (op === '>')
|
|
275
|
+
return a > b;
|
|
276
|
+
return a >= b;
|
|
277
|
+
}
|
|
278
|
+
const a = toNumber(l);
|
|
279
|
+
const b = toNumber(r);
|
|
280
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
281
|
+
return false;
|
|
282
|
+
if (op === '<')
|
|
283
|
+
return a < b;
|
|
284
|
+
if (op === '<=')
|
|
285
|
+
return a <= b;
|
|
286
|
+
if (op === '>')
|
|
287
|
+
return a > b;
|
|
288
|
+
return a >= b;
|
|
289
|
+
}
|
|
290
|
+
// Property access is case-insensitive, preferring an exact match.
|
|
291
|
+
function getProp(obj, prop) {
|
|
292
|
+
if (obj === null || typeof obj !== 'object')
|
|
293
|
+
return null;
|
|
294
|
+
if (Array.isArray(obj))
|
|
295
|
+
return null;
|
|
296
|
+
if (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
297
|
+
return obj[prop];
|
|
298
|
+
const lower = prop.toLowerCase();
|
|
299
|
+
for (const k of Object.keys(obj)) {
|
|
300
|
+
if (k.toLowerCase() === lower)
|
|
301
|
+
return obj[k];
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
function stringify(v) {
|
|
306
|
+
if (v === null)
|
|
307
|
+
return '';
|
|
308
|
+
if (typeof v === 'boolean')
|
|
309
|
+
return v ? 'true' : 'false';
|
|
310
|
+
if (typeof v === 'number')
|
|
311
|
+
return String(v);
|
|
312
|
+
if (typeof v === 'string')
|
|
313
|
+
return v;
|
|
314
|
+
return JSON.stringify(v);
|
|
315
|
+
}
|
|
316
|
+
function formatFn(args) {
|
|
317
|
+
if (args.length === 0 || typeof args[0] !== 'string')
|
|
318
|
+
throw new ExprError('format() needs a format string');
|
|
319
|
+
const fmt = args[0];
|
|
320
|
+
let out = '';
|
|
321
|
+
let i = 0;
|
|
322
|
+
while (i < fmt.length) {
|
|
323
|
+
const c = fmt[i];
|
|
324
|
+
if (c === '{') {
|
|
325
|
+
if (fmt[i + 1] === '{') {
|
|
326
|
+
out += '{';
|
|
327
|
+
i += 2;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const m = fmt.slice(i).match(/^\{(\d+)\}/);
|
|
331
|
+
if (!m)
|
|
332
|
+
throw new ExprError('malformed placeholder in format()');
|
|
333
|
+
const idx = parseInt(m[1], 10) + 1;
|
|
334
|
+
if (idx >= args.length)
|
|
335
|
+
throw new ExprError(`format() placeholder {${m[1]}} has no argument`);
|
|
336
|
+
out += stringify(args[idx]);
|
|
337
|
+
i += m[0].length;
|
|
338
|
+
}
|
|
339
|
+
else if (c === '}') {
|
|
340
|
+
if (fmt[i + 1] === '}') {
|
|
341
|
+
out += '}';
|
|
342
|
+
i += 2;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
throw new ExprError("unescaped '}' in format()");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
out += c;
|
|
350
|
+
i++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
function containsFn(hay, needle) {
|
|
356
|
+
if (Array.isArray(hay))
|
|
357
|
+
return hay.some((x) => compareEqual(x, needle));
|
|
358
|
+
return stringify(hay).toLowerCase().includes(stringify(needle).toLowerCase());
|
|
359
|
+
}
|
|
360
|
+
const BUILTINS = {
|
|
361
|
+
contains: (a, b) => containsFn(a, b),
|
|
362
|
+
startswith: (a, b) => stringify(a).toLowerCase().startsWith(stringify(b).toLowerCase()),
|
|
363
|
+
endswith: (a, b) => stringify(a).toLowerCase().endsWith(stringify(b).toLowerCase()),
|
|
364
|
+
format: (...args) => formatFn(args),
|
|
365
|
+
join: (a, sep) => {
|
|
366
|
+
const s = sep === undefined ? ',' : stringify(sep);
|
|
367
|
+
if (Array.isArray(a))
|
|
368
|
+
return a.map(stringify).join(s);
|
|
369
|
+
return stringify(a);
|
|
370
|
+
},
|
|
371
|
+
tojson: (a) => JSON.stringify(a === undefined ? null : a, null, 2),
|
|
372
|
+
fromjson: (a) => {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(stringify(a));
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
throw new ExprError(`fromJSON(): invalid JSON: ${stringify(a).slice(0, 80)}`);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
hashfiles: () => {
|
|
381
|
+
throw new ExprError('hashFiles() is not supported yet');
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
function evalNode(n, env) {
|
|
385
|
+
switch (n.t) {
|
|
386
|
+
case 'lit':
|
|
387
|
+
return n.v;
|
|
388
|
+
case 'ctx': {
|
|
389
|
+
const v = getProp(env.contexts, n.name);
|
|
390
|
+
if (v === null && !Object.keys(env.contexts).some((k) => k.toLowerCase() === n.name.toLowerCase())) {
|
|
391
|
+
throw new ExprError(`unknown context or value: ${n.name}`);
|
|
392
|
+
}
|
|
393
|
+
return v;
|
|
394
|
+
}
|
|
395
|
+
case 'not':
|
|
396
|
+
return !truthy(evalNode(n.e, env));
|
|
397
|
+
case 'bin': {
|
|
398
|
+
if (n.op === '&&') {
|
|
399
|
+
const l = evalNode(n.l, env);
|
|
400
|
+
return truthy(l) ? evalNode(n.r, env) : l;
|
|
401
|
+
}
|
|
402
|
+
if (n.op === '||') {
|
|
403
|
+
const l = evalNode(n.l, env);
|
|
404
|
+
return truthy(l) ? l : evalNode(n.r, env);
|
|
405
|
+
}
|
|
406
|
+
const l = evalNode(n.l, env);
|
|
407
|
+
const r = evalNode(n.r, env);
|
|
408
|
+
if (n.op === '==')
|
|
409
|
+
return compareEqual(l, r);
|
|
410
|
+
if (n.op === '!=')
|
|
411
|
+
return !compareEqual(l, r);
|
|
412
|
+
return compareOrder(n.op, l, r);
|
|
413
|
+
}
|
|
414
|
+
case 'deref':
|
|
415
|
+
return getProp(evalNode(n.e, env), n.prop);
|
|
416
|
+
case 'index': {
|
|
417
|
+
const obj = evalNode(n.e, env);
|
|
418
|
+
const idx = evalNode(n.i, env);
|
|
419
|
+
if (Array.isArray(obj)) {
|
|
420
|
+
const i = toNumber(idx);
|
|
421
|
+
if (!Number.isInteger(i) || i < 0 || i >= obj.length)
|
|
422
|
+
return null;
|
|
423
|
+
return obj[i];
|
|
424
|
+
}
|
|
425
|
+
return getProp(obj, stringify(idx));
|
|
426
|
+
}
|
|
427
|
+
case 'call': {
|
|
428
|
+
const fn = env.functions?.[n.name] ?? BUILTINS[n.name];
|
|
429
|
+
if (!fn)
|
|
430
|
+
throw new ExprError(`unknown function: ${n.name}`);
|
|
431
|
+
return fn(...n.args.map((a) => evalNode(a, env)));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function evaluate(expression, env) {
|
|
436
|
+
return evalNode(new Parser(tokenize(expression)).parse(), env);
|
|
437
|
+
}
|
|
438
|
+
// ---- ${{ }} interpolation over strings ----
|
|
439
|
+
// Find the ${{ ... }} spans in a string, respecting single-quoted string
|
|
440
|
+
// literals inside the expression (which may contain '}}').
|
|
441
|
+
function findSpans(s) {
|
|
442
|
+
const spans = [];
|
|
443
|
+
let i = 0;
|
|
444
|
+
while (i < s.length) {
|
|
445
|
+
const start = s.indexOf('${{', i);
|
|
446
|
+
if (start === -1)
|
|
447
|
+
break;
|
|
448
|
+
let j = start + 3;
|
|
449
|
+
let inStr = false;
|
|
450
|
+
let end = -1;
|
|
451
|
+
while (j < s.length) {
|
|
452
|
+
const c = s[j];
|
|
453
|
+
if (inStr) {
|
|
454
|
+
if (c === "'") {
|
|
455
|
+
if (s[j + 1] === "'")
|
|
456
|
+
j++;
|
|
457
|
+
else
|
|
458
|
+
inStr = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (c === "'") {
|
|
462
|
+
inStr = true;
|
|
463
|
+
}
|
|
464
|
+
else if (c === '}' && s[j + 1] === '}') {
|
|
465
|
+
end = j;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
j++;
|
|
469
|
+
}
|
|
470
|
+
if (end === -1)
|
|
471
|
+
throw new ExprError('unterminated ${{ in string');
|
|
472
|
+
spans.push({ start, end: end + 2, expr: s.slice(start + 3, end).trim() });
|
|
473
|
+
i = end + 2;
|
|
474
|
+
}
|
|
475
|
+
return spans;
|
|
476
|
+
}
|
|
477
|
+
// Replace every ${{ expr }} in a string with the stringified result.
|
|
478
|
+
function render(s, env) {
|
|
479
|
+
const spans = findSpans(s);
|
|
480
|
+
if (spans.length === 0)
|
|
481
|
+
return s;
|
|
482
|
+
let out = '';
|
|
483
|
+
let pos = 0;
|
|
484
|
+
for (const sp of spans) {
|
|
485
|
+
out += s.slice(pos, sp.start);
|
|
486
|
+
out += stringify(evaluate(sp.expr, env));
|
|
487
|
+
pos = sp.end;
|
|
488
|
+
}
|
|
489
|
+
return out + s.slice(pos);
|
|
490
|
+
}
|
|
491
|
+
// Render every string leaf of a template value (e.g. an env map).
|
|
492
|
+
function renderDeep(v, env) {
|
|
493
|
+
if (typeof v === 'string')
|
|
494
|
+
return render(v, env);
|
|
495
|
+
if (Array.isArray(v))
|
|
496
|
+
return v.map((x) => renderDeep(x, env));
|
|
497
|
+
if (typeof v === 'object' && v !== null) {
|
|
498
|
+
const out = {};
|
|
499
|
+
for (const [k, val] of Object.entries(v))
|
|
500
|
+
out[k] = renderDeep(val, env);
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
return v === undefined ? null : v;
|
|
504
|
+
}
|
|
505
|
+
// An `if:` condition: bare expressions are wrapped implicitly; a condition
|
|
506
|
+
// that is exactly one ${{ }} evaluates to its value; a mixed string renders
|
|
507
|
+
// first and the resulting text is judged (GitHub's much-lamented behavior,
|
|
508
|
+
// reproduced here so workflows behave identically).
|
|
509
|
+
function evalCondition(cond, env) {
|
|
510
|
+
const trimmed = cond.trim();
|
|
511
|
+
if (trimmed === '')
|
|
512
|
+
return true;
|
|
513
|
+
if (!trimmed.includes('${{'))
|
|
514
|
+
return truthy(evaluate(trimmed, env));
|
|
515
|
+
const spans = findSpans(trimmed);
|
|
516
|
+
if (spans.length === 1 && spans[0].start === 0 && spans[0].end === trimmed.length) {
|
|
517
|
+
return truthy(evaluate(spans[0].expr, env));
|
|
518
|
+
}
|
|
519
|
+
const rendered = render(trimmed, env).trim().toLowerCase();
|
|
520
|
+
return rendered !== '' && rendered !== 'false' && rendered !== '0' && rendered !== 'null';
|
|
521
|
+
}
|
|
522
|
+
// Whether a condition references a status function that overrides the
|
|
523
|
+
// default success() gate (always(), failure(), cancelled()).
|
|
524
|
+
function mentionsStatusOverride(cond) {
|
|
525
|
+
return /\b(always|failure|cancelled)\s*\(/.test(cond);
|
|
526
|
+
}
|