v0.1.0 28f9d6e

oracle.mjs RawHistoryFolder

1// SPDX-License-Identifier: MIT
2//
3// Generates testdata/oracle.json.gz: the results of the reference implementation
4// (@actions/expressions 0.3.61, MIT, GitHub) on a corpus of expressions. The Go
5// tests (oracle_test.go) replay the corpus and compare token by token what
6// GitHub's JavaScript library does with what this package does.
7//
8//   node gen/oracle.mjs /path/to/@actions/expressions/dist [/path/to/actions-expressions-X.tgz] \\
9//     | gzip -9 -n > testdata/oracle.json.gz
10//
11// The optional tarball is the npm package the dist directory came from: its
12// sha512 (base64, the "integrity" field of the npm registry) is recorded next
13// to the Node version in the output, so the provenance of the oracle is
14// checkable.
15//
16// The corpus is hand-written edge cases plus a seeded pseudo-random walk over
17// the grammar and over mutations of it, so the file is reproducible.
18
19import { createHash } from "node:crypto";
20import { readFileSync } from "node:fs";
21import { pathToFileURL } from "node:url";
22import path from "node:path";
23
24const dist = path.resolve(process.argv[2] ?? "node_modules/@actions/expressions/dist");
25const ex = await import(pathToFileURL(path.join(dist, "index.js")).href);
26const { Lexer, Parser, Evaluator, data } = ex;
27
28const pkgJson = JSON.parse(readFileSync(path.join(dist, "..", "package.json"), "utf8"));
29const integrity = process.argv[3]
30  ? "sha512-" + createHash("sha512").update(readFileSync(process.argv[3])).digest("base64")
31  : "";
32const reference = {
33  package: `${pkgJson.name} ${pkgJson.version}`,
34  integrity,
35  node: process.version,
36  v8: process.versions.v8,
37  unicode: process.versions.unicode,
38};
39
40// ---- contexts -------------------------------------------------------------
41
42const contexts = {
43  github: {
44    event_name: "push",
45    ref: "refs/heads/main",
46    sha: "0123abc",
47    actor: "Octocat",
48    repository: "octo/Hello-World",
49    run_number: 7,
50    run_id: "1658821493",
51    event: {
52      number: 12,
53      commits: [
54        { id: "1", message: "fix: bug", author: { name: "Ann", email: "a@x" }, added: ["a.txt"] },
55        { id: "2", message: "feat: thing", author: { name: "Bob" }, added: [] },
56        { id: "3", message: "Chore", added: ["b", "c"] },
57      ],
58      head_commit: { message: "hello", timestamp: null },
59      labels: [{ name: "bug" }, { name: "Docs" }, { name: "help wanted" }],
60      pull_request: { draft: false, title: "A title", labels: [{ name: "x" }] },
61      nested: [[1, 2], [3], [], [4, 5, 6]],
62    },
63    "weird-key": 1,
64    "a b": "space",
65  },
66  env: { FOO: "bar", EMPTY: "", NUM: "42", TRUE: "true", JSONTEXT: '{"a":[1,2,3]}', Mixed: "MiXeD", Float: "1.50", Hex: "0x1F" },
67  matrix: {
68    os: "ubuntu",
69    node: [14, 16, 18],
70    include: [{ os: "windows", node: 20 }, { os: "mac", experimental: true }],
71    v: 1.5,
72    flag: true,
73    nothing: null,
74    ver: "3.10",
75    big: 12345678901234567890,
76    neg: -0.25,
77    tiny: 1e-7,
78    zero: 0,
79    obj: { k: "v" },
80  },
81  inputs: { name: "World", count: 3, deploy: false, list: ["a", "b"], empty: {} },
82  steps: { "10": { outputs: { r: "ten" } }, "2": { outputs: { r: "two" } }, build: { outputs: { r: "b", Other: "o" }, outcome: "success" } },
83  needs: {},
84  vars: { A: "1", B: "two" },
85};
86const ctxNames = Object.keys(contexts);
87
88const extFuncs = [
89  { name: "success", description: "", minArgs: 0, maxArgs: 0, call: () => new data.BooleanData(true) },
90  { name: "always", description: "", minArgs: 0, maxArgs: 0, call: () => new data.BooleanData(true) },
91  {
92    name: "hashFiles",
93    description: "",
94    minArgs: 1,
95    maxArgs: 255,
96    call: (...a) => new data.StringData("hash:" + a.map(x => x.coerceString()).join("|")),
97  },
98];
99const extInfos = extFuncs.map(f => ({ name: f.name, description: f.description, minArgs: f.minArgs, maxArgs: f.maxArgs }));
100const funcMap = new Map(extFuncs.map(f => [f.name.toLowerCase(), f]));
101
102const rootDict = new data.Dictionary(
103  ...Object.keys(contexts).map(k => ({ key: k, value: JSON.parse(JSON.stringify(contexts[k]), data.reviver) }))
104);
105
106// ---- running the reference -------------------------------------------------
107
108function kindOf(v) {
109  switch (v.kind) {
110    case data.Kind.Null: return "null";
111    case data.Kind.Boolean: return "boolean";
112    case data.Kind.Number: return "number";
113    case data.Kind.String: return "string";
114    case data.Kind.Array: return "array";
115    case data.Kind.Dictionary: return "object";
116  }
117  return "?";
118}
119
120function run(expr) {
121  const out = { e: expr };
122  let tokens;
123  try {
124    tokens = new Lexer(expr).lex().tokens;
125  } catch (e) {
126    if (!(e instanceof Error) || e instanceof TypeError || e instanceof RangeError) { out.skip = String(e); return out; }
127    out.lexErr = e.message;
128    return out;
129  }
130  out.tokens = tokens.map(t => [t.type, t.lexeme, t.range.start.line, t.range.start.column, t.range.end.line, t.range.end.column,
131    t.type === 17 ? (Number.isNaN(t.value) ? "NaN" : String(t.value)) : t.type === 18 ? t.value : null]);
132  let ast;
133  try {
134    ast = new Parser(tokens, ctxNames, extInfos).parse();
135  } catch (e) {
136    if (e instanceof TypeError || e instanceof RangeError) { out.skip = String(e); return out; }
137    out.parseErr = e.message;
138    if (e.pos) out.pos = [e.pos.line, e.pos.column];
139    return out;
140  }
141  if (ast === undefined) { out.empty = true; return out; }
142  let res;
143  try {
144    res = new Evaluator(ast, rootDict, funcMap).evaluate();
145  } catch (e) {
146    if (e instanceof TypeError || e instanceof RangeError) { out.skip = String(e); return out; }
147    out.evalErr = e.message;
148    return out;
149  }
150  if (res === undefined) { out.skip = "undefined result"; return out; }
151  const str = res.coerceString();
152  out.r = { k: kindOf(res), s: str, j: JSON.stringify(res, data.replacer, "  ") };
153  if (!str.isWellFormed()) {
154    // A string holding a lone surrogate cannot travel through JSON text and
155    // UTF-8: give its UTF-16 code units instead.
156    out.r.s = "";
157    out.r.u = Array.from({ length: str.length }, (_, i) => str.charCodeAt(i));
158  }
159  return out;
160}
161
162// ---- corpus ----------------------------------------------------------------
163
164const corpus = [];
165const add = (...xs) => corpus.push(...xs);
166
167// literals, numbers
168add("true", "false", "null", "NaN", "Infinity", "-Infinity", "+Infinity", "1", "-1", "+1", "1.5", ".5", "-.5", "5.", "1e3", "1E-3", "1e21", "1e-7",
169  "0x10", "0X1f", "0o17", "0b101", "-0x10", "0x", "0b2", "1_000", "1e", "1e+", "--1", "+-1", "-", "+", ".", "..", "1.2.3", "0.1", "0.30000000000000004",
170  "100000000000000000000", "1000000000000000000000", "123456789012345678", "-0", "0", "00", "007", "1e308", "1e309", "-1e309", "4.9e-324",
171  "0.000001", "0.0000001", "123456789.123456789", "0.1234567890123456789", "9.9999999999999995", "0.9999999999999999", "0.99999999999999999",
172  "1.0000000000000002", "2.5", "0.5", "1.005", "1.45", "8.345", "0.000000000000005", "0.0000000000000005", "0.0000000000000004999",
173  "1e15", "1e16", "123456789012345.6", "12345678901234567.5");
174for (const n of ["0.1", "0.5", "1.5", "2.5", "1e21", "1e-7", "1e300", "-1e-7", "0.000001", "123456789012345678901", "5e-324", "1.7976931348623157e308",
175  "0.30000000000000004", "1.005", "0.1234567890123456", "0.12345678901234567", "1234567.1234567890123456", "-0.0000000000000005", "0.0000000000000005",
176  "NaN", "Infinity", "-Infinity", "100", "1e20", "123e-20", "99999999999999999999", "0.000000000000001", "0.0000000000000011", "4503599627370497.5",
177  "0.45", "0.55", "-1.5", "1.0000000000000005", "1.0000000000000004", "10.000000000000005"]) {
178  add(`format('{0}', ${n})`, `toJSON(${n})`, `${n} == '${n}'`, `'${n}' == ${n}`, `join(fromJSON('[${n}]'), ',')`);
179}
180
181// strings
182add("'a'", "''", "''''", "'it''s'", "'a''b''c'", "'''a'", "'a'''", "'a''''b'", "'unterminated", "'", "'a'b", "'a' 'b'", "'multi\nline'",
183  "'\u00e9'", "'\u{1F600}'", "'\u{1F600}' == '\u{1F600}'", "\"dq\"", "'tab\there'");
184
185// whitespace & lexing oddities
186add("  true  ", "\ttrue\r\n", "\n\ntrue", "\u00a0true", "true\u00a0", "tr\u00a0ue", "\vtrue", "true\u2028", "true\ufeff", "a", "github", "GITHUB", "GitHub.Event_Name",
187  "github .event_name", "github. event_name", "github.\nevent_name", "github\n.event_name", "github.event_name.", "github..event_name", ".github", "github.",
188  "github[", "github[]", "github['event_name'", "github['event_name']]", "github[0]", "github[-1]", "github[1.5]", "github['0']", "github[true]", "github[null]",
189  "github[nan]", "github[NaN]", "github[Infinity]", "github[github]", "github[github.event]", "github['EVENT_NAME']", "github[ 'event_name' ]",
190  "=", "==", "===", "= =", "&", "&&", "&&&", "|", "||", "|||", "!", "!!", "!!!true", "!=", "!==", "!==true", "<", "<=", "<=>", ">", ">=", ">==",
191  "1 = 1", "1 & 1", "1 | 1", "1 &&", "&& 1", "1 && && 1", "1 ||", "|| 1", "@", "#", "$", "1 + 1", "1 - 1", "1 * 1", "1 / 1", "a-b", "a_b", "a1", "1a", "_a", "-a", "a-",
192  "github-", "github-x", "github.event-name", "github.event_name-x", "github.0", "github.1a", "github.-x", "github.true", "github.null", "github.NaN", "github.Infinity",
193  "github.false", "env.TRUE", "env.true", "env.null", "(", ")", "()", "(())", "(1", "1)", "((1)", "(1))", "(1)(2)", "1 2", "1,2", ",", "1,", "[", "]", "[]", "[1]", "{}",
194  "*", "*.a", "1.*", "'a'.b", "'a'[0]", "(github).event_name", "(github)['event_name']", "(github.event).number", "(1).a", "(1)[0]", "1[0]", "true.a", "null.a", "null[0]",
195  "true[0]", "'abc'[0]", "'abc'.length", "(('a'))[0]", "toJSON(1)[0]", "fromJSON('[1,2]')[1]", "fromJSON('{\"a\":1}').a", "fromJSON('{\"a\":1}')['A']", "fromJSON('[[1]]')[0][0]");
196
197// operators, coercions
198const vals = ["null", "true", "false", "0", "1", "-1", "0.5", "NaN", "Infinity", "''", "'0'", "'1'", "' 1 '", "'abc'", "'ABC'", "'a'", "'b'", "'true'", "'null'", "'NaN'", "'Infinity'", "'0x10'", "'1e2'", "'-1'", "'1.0'", "'\u00df'", "'SS'", "'ss'", "'\u0131'", "'I'", "'i'", "'\u0130'", "'\u03c3'", "'\u03c2'", "'\u03a3'", "'\ufb03'", "'\u212a'", "'k'", "'K'", "'\u{1F600}'", "'\ue000'", "'\u{10000}'", "github", "github.event.commits", "github.event.commits[0]", "env", "matrix.node", "matrix.obj", "inputs.empty", "fromJSON('[]')", "fromJSON('{}')", "github.event.commits.*", "github.event.nope.*", "matrix.nothing", "env.NUM", "env.EMPTY"];
199for (const a of vals) {
200  add(`!${a}`, `!!${a}`, `${a} || 'dflt'`, `${a} && 'then'`, `format('{0}', ${a})`, `toJSON(${a})`, `join(${a})`, `join(${a}, '-')`, `fromJSON(${a})`, `case(${a}, 'x', 'y', 'z')`, `case(${a} == ${a}, 1, 2, 3)`);
201  for (const b of vals) {
202    for (const op of ["==", "!=", "<", "<=", ">", ">="]) add(`${a} ${op} ${b}`);
203    add(`contains(${a}, ${b})`, `startsWith(${a}, ${b})`, `endsWith(${a}, ${b})`);
204  }
205}
206
207// precedence and chains
208add("true || false && false", "(true || false) && false", "false && true || true", "1 == 1 == true", "1 < 2 == true", "true == 1 < 2", "!1 == 2", "!(1 == 2)", "!1 < 2", "1 < !2", "!true == false",
209  "1 || 2 || 3", "0 || '' || null", "0 || '' || 'x'", "1 && 2 && 3", "1 && 0 && 3", "null && 1", "null || null", "'a' && 'b' || 'c'", "'' && 'b' || 'c'", "a && b", "true && (false || true)",
210  "1 == 1 && 2 == 2 || 3 == 4", "1 != 1 || 2 != 2", "github.event_name == 'push' && contains(github.ref, 'main')", "github.event_name != 'push' || !contains(github.ref, 'dev')",
211  "contains(github.event.commits.*.message, 'fix: bug')", "contains(github.event.commits.*.message, 'FIX: BUG')", "contains(toJSON(github.event.commits), 'Bob')",
212  "github.event.commits[0].author.name", "github.event.commits[1].author.email", "github.event.commits[2].author.name", "github.event.commits[3]", "github.event.commits[-1]", "github.event.commits['1'].id",
213  "github.event.commits[1.9].id", "github.event.commits['1.9'].id", "github.event.commits[' 1 '].id", "github.event.commits[true]", "github.event.commits[false].id", "github.event.commits[null].id",
214  "github.event.commits[1e0].id", "github.event.commits[0x1].id", "github.event.commits[Infinity]", "github.event.commits[-0].id", "github.event.commits[1e400]", "github.event.commits[1e20]",
215  "github.event.commits.*", "github.event.commits.*.id", "github.event.commits.*.author", "github.event.commits.*.author.name", "github.event.commits.*.author.email", "github.event.commits.*.added",
216  "github.event.commits.*.added.*", "github.event.commits.*.added[0]", "github.event.commits.*.added[1]", "github.event.commits.*.*", "github.event.commits[*].id", "github.event.commits[*]",
217  "github.event.commits[*][*]", "github.event.commits.*[0]", "github.event.commits.*['id']", "github.event.commits.*.author.*", "github.*", "github.event.*", "github.*.number", "github.*.*", "github.event.*.*",
218  "github.event.nested.*", "github.event.nested.*.*", "github.event.nested[*][0]", "github.event.nested.*[1]", "github.event.nested.*.*.*", "github.event.nested[3][1]", "github.event.nested[2][0]",
219  "github.event.labels.*.name", "join(github.event.labels.*.name, ', ')", "join(github.event.labels.*.name)", "toJSON(github.event.labels.*.name)", "toJSON(github.event.labels.*)",
220  "github.event.head_commit.*", "github.event.head_commit.message", "github.event.head_commit.timestamp", "github.event.head_commit.timestamp.x", "github.event.head_commit.timestamp.*",
221  "github.event.pull_request.draft", "github.event.pull_request.draft == false", "!github.event.pull_request.draft", "github.event.pull_request.labels.*.name",
222  "github['weird-key']", "github.weird-key", "github['a b']", "github.a b", "github.EVENT.NUMBER", "github.Event['Number']", "github.event.number == 12", "github.event.number == '12'", "github.event.number > 5",
223  "github.event.number > '5'", "github.event.number > 'x'", "github.run_number", "github.run_id", "github.run_id == 1658821493", "github.run_id > 1658821492",
224  "env.FOO", "env.foo", "env.Mixed == 'mixed'", "env.NUM == 42", "env.NUM > 41", "env.NUM + 1", "env.Float == 1.5", "env.Hex == 31", "env.TRUE == true", "env.EMPTY == null", "env.EMPTY == false", "env.EMPTY",
225  "fromJSON(env.JSONTEXT).a[1]", "fromJSON(env.JSONTEXT).a.*", "toJSON(fromJSON(env.JSONTEXT))",
226  "matrix.os", "matrix.node", "matrix.node[0]", "matrix.node.*", "matrix.include.*.os", "matrix.include[0].node", "matrix.include[1].experimental", "matrix.v", "matrix.flag", "matrix.nothing", "matrix.neg", "matrix.tiny",
227  "matrix.big", "matrix.big == 12345678901234567890", "format('{0}', matrix.big)", "format('{0} {1}', matrix.tiny, matrix.neg)", "matrix.ver", "matrix.ver == 3.1", "matrix.ver == '3.10'", "matrix.zero || 'z'",
228  "matrix.obj", "matrix.obj == matrix.obj", "matrix.obj != matrix.obj", "matrix.node == matrix.node", "github.event.commits == github.event.commits", "github.event.commits.* == github.event.commits.*",
229  "matrix.obj.k", "matrix.obj['k']", "matrix.obj[0]", "matrix.obj.*", "matrix.node['0']", "matrix.node[0] == 14", "matrix.node[0] < matrix.node[1]", "contains(matrix.node, 16)", "contains(matrix.node, '16')", "contains(matrix.node, 17)",
230  "contains(matrix.include.*.os, 'MAC')", "inputs.name", "inputs.count", "inputs.count == 3", "inputs.deploy", "inputs.deploy == false", "!inputs.deploy", "inputs.list", "inputs.list.*", "inputs.empty", "inputs.empty.*", "inputs.empty.a",
231  "inputs.missing", "inputs.missing.deeper", "inputs.missing[0]", "inputs.missing.*", "inputs.missing.*.x", "inputs['missing']['x']", "steps.build.outputs.r", "steps.BUILD.OUTPUTS.OTHER", "steps.build.outcome == 'success'", "steps.build.outputs.*",
232  "steps.10.outputs.r", "steps['10'].outputs.r", "steps[10].outputs.r", "steps[2].outputs.r", "steps.*", "toJSON(steps)", "toJSON(steps.*.outputs)", "steps.*.outputs.r", "needs", "needs.x", "needs.x.result", "needs.*", "toJSON(needs)", "toJSON(needs.*)",
233  "vars.A", "vars.B", "vars.A == 1", "vars.C || 'default'", "toJSON(vars)", "toJSON(github.event.commits[0])", "toJSON(github.event)", "toJSON(contains)", "toJSON(github)", "toJSON(matrix)", "toJSON(inputs)", "toJSON(env)",
234  "toJSON(github.event.commits.*.added)", "toJSON(github.event.nested)", "toJSON(github.event.nested.*)", "toJSON(github.event.nested.*.*)", "toJSON(matrix.big)", "toJSON(matrix.tiny)", "toJSON(matrix.neg)", "toJSON(matrix.nothing)", "toJSON(inputs.empty)", "toJSON(inputs.list)");
235
236// functions: arity and behaviours
237add("contains()", "contains(1)", "contains(1,2)", "contains(1,2,3)", "contains('a','b',)", "contains(,)", "contains('abc','B')", "contains('abc','')", "contains('', '')", "contains('', 'a')", "contains(null, '')", "contains(null, null)", "contains(1234, 23)", "contains(true, 'RU')", "contains(github, 'a')",
238  "contains('a', github)", "contains(github.event.commits, 'x')", "contains('\u0130', 'i')", "contains('I', '\u0131')", "contains('\u00df', 'ss')", "contains('SS', '\u00df')", "contains('\u03a3', '\u03c3')", "contains('\u03c2', '\u03c3')", "contains('A\u03a3', 'a\u03c2')", "contains('\u212a', 'k')",
239  "contains(fromJSON('[1,\"2\",null]'), null)", "contains(fromJSON('[1,\"2\",null]'), '')", "contains(fromJSON('[1,\"2\",null]'), 2)", "contains(fromJSON('[]'), null)", "contains(fromJSON('[[1]]'), fromJSON('[1]'))", "contains(github.event.commits.*.id, 2)", "contains(github.event.commits.*.id, '2')",
240  "startsWith()", "startsWith('a')", "startsWith('abc','AB')", "startsWith('abc','')", "startsWith('', '')", "startsWith(null, '')", "startsWith(123, 12)", "startsWith(github, 'a')", "startsWith('\u0131', 'I')", "startsWith('\u0131', '\u0131')", "startsWith('i', '\u0131')", "startsWith('\u00df', 'ss')", "startsWith('SS', '\u00df')",
241  "endsWith('abc','BC')", "endsWith('abc','')", "endsWith(null, 'a')", "endsWith(1.50, '5')", "endsWith(1.50, 5)", "endsWith('\u00df', 'S')", "endsWith('abc', 'abcd')",
242  "format()", "format('a')", "format('')", "format(null)", "format(1)", "format(true)", "format('{0}')", "format('{0}', 1)", "format('{0}', 1, 2)", "format('{1}', 1)", "format('{1}', 1, 2)", "format('{0}{0}', 'x')", "format('{0}{1}{0}', 'a', 'b')", "format('{{')", "format('}}')", "format('{{}}')", "format('{{0}}', 1)",
243  "format('{{{0}}}', 1)", "format('{', 1)", "format('}', 1)", "format('{}', 1)", "format('{ 0}', 1)", "format('{0 }', 1)", "format('{0:x}', 1)", "format('{0,5}', 1)", "format('{0 anything}', 1)", "format('{00}', 1)", "format('{01}', 1, 2)", "format('{-1}', 1)", "format('{a}', 1)", "format('{0', 1)", "format('0}', 1)", "format('{{0}', 1)", "format('{0}}', 1)",
244  "format('{0}}}', 1)", "format('a{0}b{1}c', 1, 2)", "format('{0}', null)", "format('{0}', true)", "format('{0}', github)", "format('{0}', github.event.commits)", "format('{0}', matrix.node.*)", "format('{12345678901234567890123}', 1)", "format('{99999999999999999999999999999999999}', 1)", "format('{0}\n{0}', 'a')", "format('x}', 1)", "format('}{', 1)", "format('{{{{', 1)", "format('{}}', 1)", "format('{0}{{', 1)", "format('{{{0}', 1)",
245  "format('{0}', 'a', 'b', 'c')", "format('{3}', 0, 1, 2, 3)", "format('{2}', 0, 1, 2, 3)", "format('{255}', 1)", "format('\u{1F600}{0}\u{1F600}', 'x')", "format('{0}', '\u{1F600}')", "format('{ {0}', 1)", "format('  {0}  ', 1)",
246  "fromJSON()", "fromJSON('1','2')", "fromJSON('')", "fromJSON('  ')", "fromJSON('\n')", "fromJSON(null)", "fromJSON('null')", "fromJSON('true')", "fromJSON('false')", "fromJSON('1')", "fromJSON('-1.5e2')", "fromJSON('\"s\"')", "fromJSON('[1,2,3]')", "fromJSON('{\"a\":1}')", "fromJSON('{\"a\":1,\"A\":2}')", "fromJSON('{\"a\":1,\"a\":2}')", "toJSON(fromJSON('{\"a\":1,\"A\":2}'))", "toJSON(fromJSON('{\"a\":1,\"a\":2,\"b\":3}'))",
247  "toJSON(fromJSON('{\"b\":1,\"2\":2,\"1\":3,\"a\":4,\"01\":5,\"4294967294\":6,\"4294967295\":7,\"-1\":8}'))", "toJSON(fromJSON('{\"\":1}'))", "toJSON(fromJSON('{\"a\":{\"b\":{\"c\":[]}}}'))", "fromJSON('{')", "fromJSON('{\"a\"')", "fromJSON('[1,]')", "fromJSON('[1 2]')", "fromJSON('{a:1}')", "fromJSON(\"{'a':1}\")", "fromJSON('01')", "fromJSON('1.')", "fromJSON('.5')", "fromJSON('+1')", "fromJSON('NaN')", "fromJSON('Infinity')", "fromJSON('undefined')", "fromJSON('nul')", "fromJSON('truee')",
248  "fromJSON('1 2')", "fromJSON(' 1 ')", "fromJSON('\"\\u0041\"')", "fromJSON('\"\\ud83d\\ude00\"')", "fromJSON('\"\\x\"')", "fromJSON('\"a\nb\"')", "fromJSON('\"\\/\"')", "fromJSON('1e999')", "fromJSON('-1e999')", "fromJSON('-0')", "toJSON(fromJSON('-0'))", "toJSON(fromJSON('1e999'))", "toJSON(fromJSON('1E2'))", "toJSON(fromJSON('0.1e1'))", "toJSON(fromJSON('123456789012345678901234567890'))",
249  "toJSON(fromJSON('[1e21,1e-7,0.000001,100,1.5,-2.5e-10]'))", "toJSON(fromJSON('\"\\u0000\\u001f\\u007f\\u2028\\\"\\\\\"'))", "toJSON('\u0000')", "toJSON('\u001f\u007f\u0080\u2028\u2029\ufeff')", "toJSON('\"\\')", "toJSON('a\nb\tc\rd\be\ff')", "toJSON('\u00e9\u00df\u{1F600}')", "toJSON(fromJSON('[]'))", "toJSON(fromJSON('{}'))", "toJSON(fromJSON('[[],{}]'))", "toJSON(fromJSON('{\"a\":[],\"b\":{}}'))", "toJSON(fromJSON('[null]'))",
250  "toJSON()", "toJSON(1,2)", "toJSON(null)", "toJSON(true)", "toJSON(1)", "toJSON(NaN)", "toJSON(Infinity)", "toJSON(-Infinity)", "toJSON(-0)", "toJSON('x')", "toJSON('')", "toJSON(1e21)", "toJSON(1e-7)", "toJSON(0.1)", "toJSON(123456789012345680000)",
251  "join()", "join('a')", "join('a','b')", "join(null)", "join(1.50)", "join(true, 'x')", "join(github)", "join(github, 'x')", "join(fromJSON('[]'))", "join(fromJSON('[1,2,3]'))", "join(fromJSON('[1,2,3]'), '')", "join(fromJSON('[1,2,3]'), ', ')", "join(fromJSON('[1,2,3]'), 1)", "join(fromJSON('[1,2,3]'), null)", "join(fromJSON('[1,2,3]'), true)", "join(fromJSON('[1,2,3]'), github)", "join(fromJSON('[1,2,3]'), fromJSON('[]'))", "join(fromJSON('[1,2,3]'), NaN)",
252  "join(fromJSON('[null,true,\"a\",1.5,[1],{}]'))", "join(matrix.node, '.')", "join(matrix.node.*, '.')", "join(github.event.commits.*.id, '/')", "join(1,2,3)", "join(fromJSON('[\"a\",\"b\"]'), '\u{1F600}')",
253  "case()", "case(true)", "case(true, 1)", "case(true, 1, 2)", "case(false, 1, 2)", "case(true, 1, false, 2, 3)", "case(false, 1, true, 2, 3)", "case(false, 1, false, 2, 3)", "case(false, 1, false, 2, 3, 4)", "case(false, 1, false, 2, 3, 4, 5)", "case(1, 2, 3)", "case('true', 2, 3)", "case(null, 2, 3)", "case(false, 2, 3, 4)", "case(true, 2, 1, 3)", "case(true, 2, null, 3)", "case(false, 1, 'x', 3)",
254  "case(true, github.event_name, 'other')", "case(github.event_name == 'push', 'a', github.event_name == 'pr', 'b', 'c')", "CASE(true, 1, 2)", "case(true, matrix.node, 2)", "toJSON(case(true, matrix.node.*, 2))", "case(1 == 1, 'a', 'b')", "case(!0, 'a', 'b')", "case(fromJSON('true'), 'a', 'b')",
255  "CONTAINS('a','A')", "Contains('a','A')", "STARTSWITH('a','A')", "endswith('a','A')", "FORMAT('{0}',1)", "fromjson('1')", "FROMJSON('1')", "tojson(1)", "TOJSON(1)", "JOIN(fromJSON('[1]'))", "unknown()", "unknown(1)", "Constantine()", "notafunction", "success", "success()", "success(1)", "always()", "ALWAYS()", "hashFiles()", "hashFiles('a')", "hashFiles('a','b')", "hashFiles('**/*.go', '!x')", "HASHFILES('a')", "failure()", "cancelled()",
256  "success() && hashFiles('a') != ''", "contains(github.ref, 'main') && success()", "always() || false", "hashFiles(github.sha, null, 1.5)", "hashFiles(1,2,3,4,5,6,7,8,9,10)", "format()", "format(1,2,3,4)");
257add("hashFiles(" + Array.from({ length: 255 }, (_, i) => `'${i}'`).join(",") + ")", "hashFiles(" + Array.from({ length: 256 }, (_, i) => `'${i}'`).join(",") + ")");
258add("format('{0}'" + ", 1".repeat(254) + ")", "format('{0}'" + ", 1".repeat(255) + ")", "case(" + Array.from({ length: 101 }, (_, i) => i < 100 ? (i % 2 == 0 ? "false" : "1") : "'end'").join(",") + ")");
259add("contains('a', 'b',)", "contains(1,)", "contains(,1)", "contains('a' 'b')", "contains('a';'b')", "contains(('a'), ('b'))", "contains(contains('a','a'), true)", "format(format('{{0}}'), 5)", "toJSON(toJSON(1))", "fromJSON(toJSON(github.event.commits[0])).author.name", "fromJSON(fromJSON('\"[1]\"'))[0]", "fromJSON('[1,2]')[fromJSON('1')]",
260  "join(fromJSON(toJSON(github.event.labels.*.name)), '+')", "toJSON(fromJSON(toJSON(github.event.commits)))", "format('{0}', toJSON(null))", "fromJSON(format('[{0},{1}]', 1, 2))[1]", "fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a", "toJSON(fromJSON('[{\"a\":1},{\"a\":2},{\"b\":3}]').*.a)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a.*)", "toJSON(fromJSON('[{\"a\":[1,2]},{\"a\":[3]}]').*.a[0])", "toJSON(fromJSON('[[1,2],[3]]').*[0])", "toJSON(fromJSON('[[1,2],[3]]')[*][1])",
261  "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}').*.x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":{\"x\":2}}')[*].x)", "toJSON(fromJSON('{\"a\":{\"x\":1},\"b\":2}').*.x)", "toJSON(fromJSON('[1,2]').*.x)", "toJSON(fromJSON('1').*)", "toJSON(null.*)", "toJSON('abc'.*)", "toJSON(fromJSON('[]').*.a)", "toJSON(fromJSON('[]').*)", "toJSON(fromJSON('{}').*)");
262
263// lone surrogates (kept as WTF-8 by the Go port)
264{
265  const H = "fromJSON('\"\\ud800\"')", L = "fromJSON('\"\\udc00\"')", H2 = "fromJSON('\"\\ud83d\"')", L2 = "fromJSON('\"\\ude00\"')";
266  const lone = [H, L, H2, L2, "'\u{1F600}'", "'\ue000'", "'\uffff'", "'a'", "''", "'\ufffd'", "'\u{10000}'"];
267  for (const a of lone) {
268    add(`toJSON(${a})`, `format('{0}', ${a})`, `join(${a})`, `${a} || 'd'`, `startsWith(${a}, ${a})`, `case(true, ${a}, 1)`);
269    for (const b of lone) {
270      for (const op of ["==", "!=", "<", "<=", ">", ">="]) add(`${a} ${op} ${b}`);
271      add(`contains(${a}, ${b})`, `startsWith(${a}, ${b})`, `endsWith(${a}, ${b})`, `format('{0}{1}', ${a}, ${b})`, `join(fromJSON('[]'), ${a}) == ''`,
272        `toJSON(fromJSON(toJSON(${a})))`, `format('{0}', ${a}) == format('{0}', ${b})`);
273    }
274  }
275  add(`${H} == ${L}`, `${H} != ${L}`, `${H} < ${L}`, `${H} > ${L}`, `contains('x${"\u{1F600}"}', ${H2})`, `contains('x\u{1F600}', ${L2})`, `startsWith('\u{1F600}', ${H2})`, `endsWith('\u{1F600}', ${L2})`,
276    `format('{0}{1}', ${H2}, ${L2}) == '\u{1F600}'`, `format('{0}{1}', ${L2}, ${H2}) == '\u{1F600}'`, `join(fromJSON('["\\ud83d","\\ude00"]'), '') == '\u{1F600}'`,
277    `join(fromJSON('["\\ud83d","\\ude00"]'), '')`, `format('{0}{1}', ${H2}, ${L2})`, `format('{0}{1}{0}', ${H2}, ${L2})`, `toJSON(format('{0}{1}', ${H2}, ${L2}))`,
278    `toJSON(fromJSON('{"\\ud800":1,"\\ud800x":2,"a\\udc00":3}'))`, `fromJSON('{"\\ud800":1}')[${H}]`, `fromJSON('{"\\ud800":1}')[${L}]`, `toJSON(fromJSON('["\\ud800\\ud800","\\udc00\\ud800"]'))`,
279    `${H} == '\ufffd'`, `'\ufffd' == ${H}`, `'\ufffd' < ${H}`, `${H} < '\ufffd'`, `${H} < '\u{1F600}'`, `'\u{1F600}' < ${H}`, `${L} < '\ue000'`, `'\ue000' < ${L}`,
280    `fromJSON('"\\ud800\\u0041"') == 'A'`, `contains(fromJSON('"A\\ud800"'), 'a')`, `startsWith(fromJSON('"a\\ud800"'), 'A')`, `endsWith(fromJSON('"\\ud800a"'), 'A')`,
281    `contains(fromJSON('"\\ud800\\udc00"'), ${H})`, `contains(fromJSON('"\\ud800\\udc00"'), ${L})`, `startsWith(fromJSON('"\\ud800\\udc00"'), ${H})`, `endsWith(fromJSON('"\\ud800\\udc00"'), ${L})`,
282    `contains(fromJSON('["\\ud800"]'), ${H})`, `contains(fromJSON('["\\ud800"]'), ${L})`, `fromJSON('"\\ud800"') == fromJSON('"\\ud800"')`);
283}
284
285// depth and length limits
286const nest = (n, open, body, close) => open.repeat(n) + body + close.repeat(n);
287for (const n of [1, 10, 24, 25, 26, 48, 49, 50, 51, 52, 100, 200]) {
288  add(nest(n, "(", "1", ")"), "!".repeat(n) + "true", nest(n, "format(", "'x'", ")"), "github" + ".event".repeat(n), "github" + "[0]".repeat(n), "github" + "['event']".repeat(n),
289    "1" + " || 1".repeat(n), "1" + " && 1".repeat(n), "1" + " == 1".repeat(n), nest(n, "(1 || ", "1", ")"), nest(n, "!(", "1", ")"), nest(n, "(1 && ", "1", ")"), nest(n, "github[", "0", "]"), nest(n, "contains(1, ", "1", ")"));
290}
291for (const n of [20990, 21000, 21001, 22000]) {
292  add("'" + "a".repeat(n - 2) + "'", " ".repeat(n) + "1", "1" + " ".repeat(n - 1), "'" + "\u{1F600}".repeat(Math.floor((n - 2) / 2)) + "'", "'" + "\u{1F600}".repeat(Math.floor((n - 3) / 2)) + "'");
293}
294
295// positions and error messages
296for (const e of ["1 +", "a b", "github github", "github.event_name ==", "== 1", "(github.event_name", "github.event_name)", "contains('a',", "contains('a'", "contains('a' 'b')", "contains(", "github[0", "github[0)",
297  "1 ==\n 2 ===\n 3", "true &&\n  false ||\n  nope", "\n\n  bogus", "'a'\n 'b'\n  zzz", "true\n&\nfalse", "foo(", "foo", "github.event_name\n  .\n  ", "  )  ", "1 !", "1 !!", "1 ! 1", "!", "! ", "!(", "!)", "(!)", "[1]", "github.[0]", "github.*.", "github.*.*.", "github[*", "github[*]]", "github[* ]", "github[1 2]", "github[,]",
298  "1 && ( 2 ||", "1\t2", "x\ty", "github\r\n.event", "a\u00a0b", "'x' =", "'x' = 'y'", "!= 1", ">", "<<", ">>", "1 <> 2", "1 >< 2", "1 =< 2", "1 => 2", "contains(github.event_name, 'push'", "contains(github.event_name 'push')", "$ {{ 1 }}", "${{ 1 }}", "${{", "}}", "{{ 1 }}", "{ 1 }", "1 }", "a.b.c", "github.a.b.c.d", "git hub", "github github.ref"]) add(e);
299
300// ---- seeded random ----------------------------------------------------------
301
302function mulberry32(a) {
303  return function () {
304    a |= 0; a = (a + 0x6d2b79f5) | 0;
305    let t = Math.imul(a ^ (a >>> 15), 1 | a);
306    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
307    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
308  };
309}
310const rnd = mulberry32(20260512);
311const pick = a => a[Math.floor(rnd() * a.length)];
312const lits = ["true", "false", "null", "0", "1", "2", "42", "-1", "1.5", "0.1", "NaN", "Infinity", "''", "'a'", "'A'", "'abc'", "'1'", "'0'", "'true'", "'x y'", "'it''s'", "'{0}'", "'[1,2]'", "'{\"a\":1}'", "'\u00df'", "'\u0131'", "'\u03a3'", "'\u{1F600}'"];
313const paths = ["github", "github.event_name", "github.event.commits", "github.event.commits.*", "github.event.commits.*.id", "github.event.commits[0]", "github.event.commits[1].author", "github.event.commits[1].author.name", "github.event.labels.*.name", "github.event.nested.*.*", "github.event.nested[3]", "github.ref", "github.nope", "github.event.nope.deep",
314  "env", "env.FOO", "env.NUM", "env.EMPTY", "matrix", "matrix.node", "matrix.node.*", "matrix.node[1]", "matrix.v", "matrix.flag", "matrix.nothing", "matrix.include.*.os", "matrix.obj", "inputs", "inputs.count", "inputs.deploy", "inputs.list.*", "inputs.empty", "steps.build.outputs.r", "steps.*.outputs", "steps['10']", "needs", "needs.*", "vars.A", "vars.B"];
315function gen(d) {
316  const r = rnd();
317  if (d <= 0 || r < 0.3) return rnd() < 0.5 ? pick(lits) : pick(paths);
318  if (r < 0.4) return pick(paths) + (rnd() < 0.5 ? `[${gen(d - 1)}]` : rnd() < 0.5 ? ".*" : "[*]");
319  if (r < 0.5) return `!${gen(d - 1)}`;
320  if (r < 0.58) return `(${gen(d - 1)})`;
321  if (r < 0.7) return `${gen(d - 1)} ${pick(["==", "!=", "<", "<=", ">", ">="])} ${gen(d - 1)}`;
322  if (r < 0.78) return `${gen(d - 1)} ${pick(["&&", "||"])} ${gen(d - 1)}`;
323  const fn = pick([["contains", 2], ["startsWith", 2], ["endsWith", 2], ["format", 3], ["fromJSON", 1], ["toJSON", 1], ["join", 2], ["case", 3], ["hashFiles", 2], ["success", 0]]);
324  const n = fn[0] === "format" ? 1 + Math.floor(rnd() * 3) : fn[0] === "case" ? pick([1, 3, 5]) : fn[0] === "join" ? 1 + Math.floor(rnd() * 2) : fn[1];
325  const args = Array.from({ length: n }, () => gen(d - 1));
326  if (fn[0] === "format" && rnd() < 0.7) args[0] = pick(["'{0}'", "'{0}{1}'", "'a{1}b{0}c'", "'{{{0}}}'", "'{0:x}'", "'{2}'"]);
327  if (fn[0] === "case" && rnd() < 0.7) for (let i = 0; i + 1 < args.length; i += 2) args[i] = pick(["true", "false", "1 == 1", "!0", "github.event_name == 'push'"]);
328  if (fn[0] === "fromJSON" && rnd() < 0.5) args[0] = pick(["'[1,2]'", "'{\"a\":{\"b\":1}}'", "'null'", "'\"x\"'", "'1e3'", "'{'", "''", "'[{\"a\":1},{\"a\":2}]'", "toJSON(" + gen(d - 1) + ")"]);
329  return `${fn[0]}(${args.join(", ")})`;
330}
331const alphabet = ["(", ")", "[", "]", ".", ",", "!", "=", "&", "|", "<", ">", "*", "'", " ", "\n", "-", "+", "0", "1", "a", "github", "true", "null", "\u00e9", "\u{1F600}", "''", "&&", "||", "==", "!=", ">=", "<="];
332function mutate(s) {
333  const chars = Array.from(s);
334  const k = 1 + Math.floor(rnd() * 3);
335  for (let i = 0; i < k; i++) {
336    const at = Math.floor(rnd() * (chars.length + 1));
337    switch (Math.floor(rnd() * 3)) {
338      case 0: chars.splice(at, 0, pick(alphabet)); break;
339      case 1: chars.splice(at, 1); break;
340      default: chars.splice(at, 1, pick(alphabet));
341    }
342  }
343  return chars.join("");
344}
345for (let i = 0; i < 2500; i++) add(gen(1 + Math.floor(rnd() * 4)));
346for (let i = 0; i < 1500; i++) add(mutate(gen(1 + Math.floor(rnd() * 3))));
347// random doubles as literals
348for (let i = 0; i < 400; i++) {
349  const digits = Array.from({ length: 1 + Math.floor(rnd() * 20) }, () => Math.floor(rnd() * 10)).join("");
350  const dot = Math.floor(rnd() * (digits.length + 1));
351  let lit = digits.slice(0, dot) + (dot < digits.length || rnd() < 0.3 ? "." : "") + digits.slice(dot);
352  if (lit === "." || lit === "") lit = "0.5";
353  if (rnd() < 0.3) lit += "e" + (rnd() < 0.5 ? "-" : "") + Math.floor(rnd() * 30);
354  add(`format('{0}', ${lit})`, `toJSON(${lit})`);
355}
356
357const seen = new Set();
358const cases = [];
359for (const e of corpus) {
360  if (seen.has(e)) continue;
361  seen.add(e);
362  cases.push(run(e));
363}
364const skipped = cases.filter(c => c.skip).length;
365console.error(`cases=${cases.length} skipped(JS crash)=${skipped}`);
366process.stdout.write(JSON.stringify({ reference, contexts, cases: cases.filter(c => !c.skip) }) + "\n");

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close