detached 3251a55

test(expr): build the runner's Expressions2 engine and record its answers on the oracle corpus

commit
3251a5503d62c80a8bc8bd645c35a99768118545
author
brice · · 2026-10-12 17:21 UTC
parent
3a9395e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +234 -0
A expr/gen/runner/.gitignore +2 -0
@@ -0,0 +1,2 @@
1+bin/
2+obj/
@@ -0,0 +1,2 @@
1+bin/
2+obj/
A expr/gen/runner/Program.cs +126 -0
@@ -0,0 +1,126 @@
1+// SPDX-License-Identifier: MIT
2+
3+using System;
4+using System.Collections.Generic;
5+using System.IO;
6+using System.Linq;
7+using System.Text;
8+using GitHub.DistributedTask.Expressions2;
9+using GitHub.DistributedTask.Expressions2.Sdk;
10+using GitHub.DistributedTask.Expressions2.Sdk.Functions;
11+using GitHub.DistributedTask.Pipelines.ContextData;
12+using Newtonsoft.Json;
13+using Newtonsoft.Json.Linq;
14+
15+// Differential harness: runs expressions through the runner's own
16+// Expressions2 code (compiled from the runner repository) and prints one JSON
17+// line per case.
18+//
19+// Input (argv[0]): JSON {"contexts": {name: json}, "cases": [{"e": expr, "ctx": {name: json} | null}]}
20+// Output: JSON lines {"k": kind, "s": string form, "j": toJson, "err": {"t": "parse"|"eval", "m": message}}
21+
22+internal sealed class CtxNode : NamedValue
23+{
24+ protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory)
25+ {
26+ resultMemory = null;
27+ return (context.State as DictionaryContextData)[Name];
28+ }
29+}
30+
31+internal sealed class Success : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } }
32+internal sealed class Always : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } }
33+internal sealed class HashFiles : Function
34+{
35+ protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m)
36+ {
37+ m = null;
38+ var s = "hash:";
39+ for (var i = 0; i < Parameters.Count; i++)
40+ {
41+ if (i > 0) s += "|";
42+ s += Parameters[i].Evaluate(c).ConvertToString();
43+ }
44+ return s;
45+ }
46+}
47+
48+internal static class P
49+{
50+ static DictionaryContextData Load(JObject ctx)
51+ {
52+ var d = new DictionaryContextData();
53+ foreach (var p in ctx.Properties())
54+ {
55+ var token = p.Value;
56+ d[p.Name] = token.ToPipelineContextData();
57+ }
58+ return d;
59+ }
60+
61+ static int Main(string[] args)
62+ {
63+ JObject root; using (var jr0 = new JsonTextReader(new StringReader(File.ReadAllText(args[0]))) { DateParseHandling = DateParseHandling.None }) root = JObject.Load(jr0);
64+ var defaultCtx = (JObject)root["contexts"];
65+ var funcs = new IFunctionInfo[]
66+ {
67+ new FunctionInfo<Success>("success", 0, 0),
68+ new FunctionInfo<Always>("always", 0, 0),
69+ new FunctionInfo<HashFiles>("hashFiles", 1, 255),
70+ };
71+ var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
72+ var defaultData = Load(defaultCtx);
73+ foreach (var c in (JArray)root["cases"])
74+ {
75+ var e = (string)c["e"];
76+ var custom = c["ctx"] as JObject;
77+ var data = custom != null ? Load(custom) : defaultData;
78+ var names = (custom ?? defaultCtx).Properties().Select(p => p.Name);
79+ var res = new JObject();
80+ IExpressionNode tree = null;
81+ try
82+ {
83+ tree = new ExpressionParser().CreateTree(e, null, names.Select(n => (INamedValueInfo)new NamedValueInfo<CtxNode>(n)).ToList(), funcs);
84+ }
85+ catch (Exception ex)
86+ {
87+ res["err"] = new JObject { ["t"] = "parse", ["m"] = ex.Message, ["x"] = ex.GetType().Name };
88+ }
89+ if (res["err"] == null)
90+ {
91+ if (tree == null)
92+ {
93+ res["empty"] = true;
94+ }
95+ else
96+ {
97+ try
98+ {
99+ var r = tree.Evaluate(null, null, data, null);
100+ res["k"] = r.Kind.ToString();
101+ res["s"] = r.ConvertToString();
102+ // toJson of the result
103+ var tj = new ToJson();
104+ tj.AddParameter(new Literal(r.Value));
105+ try
106+ {
107+ var jr = ((IExpressionNode)tj).Evaluate(null, null, data, null);
108+ res["j"] = jr.ConvertToString();
109+ }
110+ catch (Exception ex2)
111+ {
112+ res["jerr"] = ex2.Message;
113+ }
114+ }
115+ catch (Exception ex)
116+ {
117+ res["err"] = new JObject { ["t"] = "eval", ["m"] = ex.Message, ["x"] = ex.GetType().Name };
118+ }
119+ }
120+ }
121+ output.WriteLine(res.ToString(Formatting.None));
122+ }
123+ output.Flush();
124+ return 0;
125+ }
126+}
@@ -0,0 +1,126 @@
1+// SPDX-License-Identifier: MIT
2+
3+using System;
4+using System.Collections.Generic;
5+using System.IO;
6+using System.Linq;
7+using System.Text;
8+using GitHub.DistributedTask.Expressions2;
9+using GitHub.DistributedTask.Expressions2.Sdk;
10+using GitHub.DistributedTask.Expressions2.Sdk.Functions;
11+using GitHub.DistributedTask.Pipelines.ContextData;
12+using Newtonsoft.Json;
13+using Newtonsoft.Json.Linq;
14+
15+// Differential harness: runs expressions through the runner's own
16+// Expressions2 code (compiled from the runner repository) and prints one JSON
17+// line per case.
18+//
19+// Input (argv[0]): JSON {"contexts": {name: json}, "cases": [{"e": expr, "ctx": {name: json} | null}]}
20+// Output: JSON lines {"k": kind, "s": string form, "j": toJson, "err": {"t": "parse"|"eval", "m": message}}
21+
22+internal sealed class CtxNode : NamedValue
23+{
24+ protected override Object EvaluateCore(EvaluationContext context, out ResultMemory resultMemory)
25+ {
26+ resultMemory = null;
27+ return (context.State as DictionaryContextData)[Name];
28+ }
29+}
30+
31+internal sealed class Success : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } }
32+internal sealed class Always : Function { protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m) { m = null; return true; } }
33+internal sealed class HashFiles : Function
34+{
35+ protected override Object EvaluateCore(EvaluationContext c, out ResultMemory m)
36+ {
37+ m = null;
38+ var s = "hash:";
39+ for (var i = 0; i < Parameters.Count; i++)
40+ {
41+ if (i > 0) s += "|";
42+ s += Parameters[i].Evaluate(c).ConvertToString();
43+ }
44+ return s;
45+ }
46+}
47+
48+internal static class P
49+{
50+ static DictionaryContextData Load(JObject ctx)
51+ {
52+ var d = new DictionaryContextData();
53+ foreach (var p in ctx.Properties())
54+ {
55+ var token = p.Value;
56+ d[p.Name] = token.ToPipelineContextData();
57+ }
58+ return d;
59+ }
60+
61+ static int Main(string[] args)
62+ {
63+ JObject root; using (var jr0 = new JsonTextReader(new StringReader(File.ReadAllText(args[0]))) { DateParseHandling = DateParseHandling.None }) root = JObject.Load(jr0);
64+ var defaultCtx = (JObject)root["contexts"];
65+ var funcs = new IFunctionInfo[]
66+ {
67+ new FunctionInfo<Success>("success", 0, 0),
68+ new FunctionInfo<Always>("always", 0, 0),
69+ new FunctionInfo<HashFiles>("hashFiles", 1, 255),
70+ };
71+ var output = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false));
72+ var defaultData = Load(defaultCtx);
73+ foreach (var c in (JArray)root["cases"])
74+ {
75+ var e = (string)c["e"];
76+ var custom = c["ctx"] as JObject;
77+ var data = custom != null ? Load(custom) : defaultData;
78+ var names = (custom ?? defaultCtx).Properties().Select(p => p.Name);
79+ var res = new JObject();
80+ IExpressionNode tree = null;
81+ try
82+ {
83+ tree = new ExpressionParser().CreateTree(e, null, names.Select(n => (INamedValueInfo)new NamedValueInfo<CtxNode>(n)).ToList(), funcs);
84+ }
85+ catch (Exception ex)
86+ {
87+ res["err"] = new JObject { ["t"] = "parse", ["m"] = ex.Message, ["x"] = ex.GetType().Name };
88+ }
89+ if (res["err"] == null)
90+ {
91+ if (tree == null)
92+ {
93+ res["empty"] = true;
94+ }
95+ else
96+ {
97+ try
98+ {
99+ var r = tree.Evaluate(null, null, data, null);
100+ res["k"] = r.Kind.ToString();
101+ res["s"] = r.ConvertToString();
102+ // toJson of the result
103+ var tj = new ToJson();
104+ tj.AddParameter(new Literal(r.Value));
105+ try
106+ {
107+ var jr = ((IExpressionNode)tj).Evaluate(null, null, data, null);
108+ res["j"] = jr.ConvertToString();
109+ }
110+ catch (Exception ex2)
111+ {
112+ res["jerr"] = ex2.Message;
113+ }
114+ }
115+ catch (Exception ex)
116+ {
117+ res["err"] = new JObject { ["t"] = "eval", ["m"] = ex.Message, ["x"] = ex.GetType().Name };
118+ }
119+ }
120+ }
121+ output.WriteLine(res.ToString(Formatting.None));
122+ }
123+ output.Flush();
124+ return 0;
125+ }
126+}
A expr/gen/runner/README.md +18 -0
@@ -0,0 +1,18 @@
1+# Running the runner's expression engine
2+
3+`runner.csproj` compiles `src/Sdk/DTExpressions2` (the Expressions2 engine the
4+runner uses on github.com), its `ContextData` types and `DTLogging` from a clone
5+of https://github.com/actions/runner, replacing the few assemblies they need by
6+the stubs in `Stubs.cs`. `Program.cs` reads
7+`{"contexts": {...}, "cases": [{"e": "<expression>"}]}` and prints one JSON line
8+per case: `{"k": kind, "s": string form, "j": toJSON of the result}`, or
9+`{"err": {"t": "parse"|"eval", "m": message}}`.
10+
11+ dotnet build -c Release expr/gen/runner -p:RunnerSrc=/path/to/runner/src/Sdk
12+ dotnet expr/gen/runner/bin/Release/net9.0/runner.dll input.json
13+
14+It is how the C# column of `expr/PORTING.md` was established: the 34 765
15+expressions of `testdata/oracle.json.gz` were run through it and compared with
16+this package. Requires the .NET SDK; nothing in `go test` depends on it.
17+The stubs are not the runner's code: `JsonUtility.ToString` is
18+`JsonConvert.SerializeObject` and the error resources are copied texts.
@@ -0,0 +1,18 @@
1+# Running the runner's expression engine
2+
3+`runner.csproj` compiles `src/Sdk/DTExpressions2` (the Expressions2 engine the
4+runner uses on github.com), its `ContextData` types and `DTLogging` from a clone
5+of https://github.com/actions/runner, replacing the few assemblies they need by
6+the stubs in `Stubs.cs`. `Program.cs` reads
7+`{"contexts": {...}, "cases": [{"e": "<expression>"}]}` and prints one JSON line
8+per case: `{"k": kind, "s": string form, "j": toJSON of the result}`, or
9+`{"err": {"t": "parse"|"eval", "m": message}}`.
10+
11+ dotnet build -c Release expr/gen/runner -p:RunnerSrc=/path/to/runner/src/Sdk
12+ dotnet expr/gen/runner/bin/Release/net9.0/runner.dll input.json
13+
14+It is how the C# column of `expr/PORTING.md` was established: the 34 765
15+expressions of `testdata/oracle.json.gz` were run through it and compared with
16+this package. Requires the .NET SDK; nothing in `go test` depends on it.
17+The stubs are not the runner's code: `JsonUtility.ToString` is
18+`JsonConvert.SerializeObject` and the error resources are copied texts.
A expr/gen/runner/Stubs.cs +28 -0
@@ -0,0 +1,28 @@
1+// SPDX-License-Identifier: MIT
2+
3+using System;
4+namespace GitHub.Services.Common {
5+ public static class ArgumentUtility {
6+ public static void CheckForNull(Object v, String name) { if (v == null) throw new ArgumentNullException(name); }
7+ public static void CheckStringForNullOrEmpty(String v, String name, bool trim = false) { if (String.IsNullOrEmpty(v)) throw new ArgumentException(name); }
8+ }
9+}
10+namespace GitHub.Services.WebApi.Internal {
11+ [AttributeUsage(AttributeTargets.All)] public sealed class ClientIgnoreAttribute : Attribute {}
12+}
13+namespace GitHub.Services.WebApi {
14+ public abstract class VssSecureJsonConverter : Newtonsoft.Json.JsonConverter { public override void WriteJson(Newtonsoft.Json.JsonWriter w, object v, Newtonsoft.Json.JsonSerializer s) {} }
15+ public static class JsonUtility {
16+ // Same as the runner's JsonUtility.ToString for a string: default JsonTextWriter settings.
17+ public static String ToString(Object o) { return Newtonsoft.Json.JsonConvert.SerializeObject(o); }
18+ }
19+}
20+namespace GitHub.DistributedTask.Expressions {
21+ internal static class ExpressionResources {
22+ public static String ExceededAllowedMemory(object e) { return "The maximum allowed memory size was exceeded while evaluating the following expression: " + e; }
23+ public static String InvalidFormatArgIndex(object f) { return "The following format string references more arguments than were supplied: " + f; }
24+ public static String InvalidFormatSpecifiers(object s, object k) { return "The format specifiers '" + s + "' are not valid for objects of type '" + k + "'"; }
25+ public static String InvalidFormatString(object f) { return "The following format string is invalid: " + f; }
26+ }
27+}
28+namespace GitHub.Actions.Expressions.Data { internal class Dummy {} }
@@ -0,0 +1,28 @@
1+// SPDX-License-Identifier: MIT
2+
3+using System;
4+namespace GitHub.Services.Common {
5+ public static class ArgumentUtility {
6+ public static void CheckForNull(Object v, String name) { if (v == null) throw new ArgumentNullException(name); }
7+ public static void CheckStringForNullOrEmpty(String v, String name, bool trim = false) { if (String.IsNullOrEmpty(v)) throw new ArgumentException(name); }
8+ }
9+}
10+namespace GitHub.Services.WebApi.Internal {
11+ [AttributeUsage(AttributeTargets.All)] public sealed class ClientIgnoreAttribute : Attribute {}
12+}
13+namespace GitHub.Services.WebApi {
14+ public abstract class VssSecureJsonConverter : Newtonsoft.Json.JsonConverter { public override void WriteJson(Newtonsoft.Json.JsonWriter w, object v, Newtonsoft.Json.JsonSerializer s) {} }
15+ public static class JsonUtility {
16+ // Same as the runner's JsonUtility.ToString for a string: default JsonTextWriter settings.
17+ public static String ToString(Object o) { return Newtonsoft.Json.JsonConvert.SerializeObject(o); }
18+ }
19+}
20+namespace GitHub.DistributedTask.Expressions {
21+ internal static class ExpressionResources {
22+ public static String ExceededAllowedMemory(object e) { return "The maximum allowed memory size was exceeded while evaluating the following expression: " + e; }
23+ public static String InvalidFormatArgIndex(object f) { return "The following format string references more arguments than were supplied: " + f; }
24+ public static String InvalidFormatSpecifiers(object s, object k) { return "The format specifiers '" + s + "' are not valid for objects of type '" + k + "'"; }
25+ public static String InvalidFormatString(object f) { return "The following format string is invalid: " + f; }
26+ }
27+}
28+namespace GitHub.Actions.Expressions.Data { internal class Dummy {} }
A expr/gen/runner/answers.mjs +37 -0
@@ -0,0 +1,37 @@
1+// SPDX-License-Identifier: MIT
2+//
3+// Generates testdata/runner.json.gz: what the C# runner's expression engine
4+// (actions/runner, src/Sdk/DTExpressions2) answers on the expressions of
5+// testdata/oracle.json.gz. Needs the .NET SDK and a clone of actions/runner:
6+//
7+// dotnet build -c Release expr/gen/runner -p:RunnerSrc=<runner>/src/Sdk
8+// node expr/gen/runner/answers.mjs <runner clone> | gzip -9 -n > expr/testdata/runner.json.gz
9+
10+import { execFileSync } from "node:child_process";
11+import { readFileSync, writeFileSync, mkdtempSync } from "node:fs";
12+import { tmpdir } from "node:os";
13+import { join, dirname } from "node:path";
14+import { fileURLToPath } from "node:url";
15+import { gunzipSync } from "node:zlib";
16+
17+const here = dirname(fileURLToPath(import.meta.url));
18+const runnerClone = process.argv[2];
19+if (!runnerClone) throw new Error("usage: answers.mjs <actions/runner clone>");
20+
21+const oracle = JSON.parse(gunzipSync(readFileSync(join(here, "../../testdata/oracle.json.gz"))));
22+const dir = mkdtempSync(join(tmpdir(), "runner-answers-"));
23+const input = join(dir, "in.json");
24+writeFileSync(input, JSON.stringify({ contexts: oracle.contexts, cases: oracle.cases.map(c => ({ e: c.e })) }));
25+const out = execFileSync("dotnet", [join(here, "bin/Release/net9.0/runner.dll"), input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean);
26+if (out.length !== oracle.cases.length) throw new Error(`got ${out.length} answers for ${oracle.cases.length} cases`);
27+const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().trim();
28+const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? "";
29+const cases = out.map((l, i) => {
30+ const r = JSON.parse(l);
31+ if (r.err) delete r.err.x; // .NET exception type name
32+ return { e: oracle.cases[i].e, ...r };
33+});
34+process.stdout.write(JSON.stringify({
35+ reference: { source: "actions/runner src/Sdk/DTExpressions2 (Expressions2)", commit: git("rev-parse", "HEAD"), runtime: dotnet.trim() },
36+ cases,
37+}));
@@ -0,0 +1,37 @@
1+// SPDX-License-Identifier: MIT
2+//
3+// Generates testdata/runner.json.gz: what the C# runner's expression engine
4+// (actions/runner, src/Sdk/DTExpressions2) answers on the expressions of
5+// testdata/oracle.json.gz. Needs the .NET SDK and a clone of actions/runner:
6+//
7+// dotnet build -c Release expr/gen/runner -p:RunnerSrc=<runner>/src/Sdk
8+// node expr/gen/runner/answers.mjs <runner clone> | gzip -9 -n > expr/testdata/runner.json.gz
9+
10+import { execFileSync } from "node:child_process";
11+import { readFileSync, writeFileSync, mkdtempSync } from "node:fs";
12+import { tmpdir } from "node:os";
13+import { join, dirname } from "node:path";
14+import { fileURLToPath } from "node:url";
15+import { gunzipSync } from "node:zlib";
16+
17+const here = dirname(fileURLToPath(import.meta.url));
18+const runnerClone = process.argv[2];
19+if (!runnerClone) throw new Error("usage: answers.mjs <actions/runner clone>");
20+
21+const oracle = JSON.parse(gunzipSync(readFileSync(join(here, "../../testdata/oracle.json.gz"))));
22+const dir = mkdtempSync(join(tmpdir(), "runner-answers-"));
23+const input = join(dir, "in.json");
24+writeFileSync(input, JSON.stringify({ contexts: oracle.contexts, cases: oracle.cases.map(c => ({ e: c.e })) }));
25+const out = execFileSync("dotnet", [join(here, "bin/Release/net9.0/runner.dll"), input], { maxBuffer: 1 << 30 }).toString("utf8").split("\n").filter(Boolean);
26+if (out.length !== oracle.cases.length) throw new Error(`got ${out.length} answers for ${oracle.cases.length} cases`);
27+const git = (...a) => execFileSync("git", ["-C", runnerClone, ...a]).toString().trim();
28+const dotnet = execFileSync("dotnet", ["--list-runtimes"]).toString().split("\n").find(l => l.includes("Microsoft.NETCore.App 9")) ?? "";
29+const cases = out.map((l, i) => {
30+ const r = JSON.parse(l);
31+ if (r.err) delete r.err.x; // .NET exception type name
32+ return { e: oracle.cases[i].e, ...r };
33+});
34+process.stdout.write(JSON.stringify({
35+ reference: { source: "actions/runner src/Sdk/DTExpressions2 (Expressions2)", commit: git("rev-parse", "HEAD"), runtime: dotnet.trim() },
36+ cases,
37+}));
A expr/gen/runner/runner.csproj +23 -0
@@ -0,0 +1,23 @@
1+<Project Sdk="Microsoft.NET.Sdk">
2+ <!-- Builds the runner's own Expressions2 sources (actions/runner, MIT) with
3+ small stubs for the assemblies they depend on, and a driver that
4+ evaluates expressions with them. See README.md. -->
5+ <PropertyGroup>
6+ <OutputType>Exe</OutputType>
7+ <TargetFramework>net9.0</TargetFramework>
8+ <Nullable>disable</Nullable>
9+ <LangVersion>11.0</LangVersion>
10+ <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn>
11+ <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
12+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
13+ <RunnerSrc Condition="'$(RunnerSrc)' == ''">$(HOME)/dev/clones/runner/src/Sdk</RunnerSrc>
14+ </PropertyGroup>
15+ <ItemGroup>
16+ <Compile Include="$(RunnerSrc)/DTExpressions2/**/*.cs" />
17+ <Compile Include="$(RunnerSrc)/DTLogging/**/*.cs" />
18+ <Compile Include="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/*.cs" Exclude="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/PipelineContextDataExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateMemoryExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateTokenExtensions.cs" />
19+ <Compile Include="Stubs.cs" />
20+ <Compile Include="Program.cs" />
21+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
22+ </ItemGroup>
23+</Project>
@@ -0,0 +1,23 @@
1+<Project Sdk="Microsoft.NET.Sdk">
2+ <!-- Builds the runner's own Expressions2 sources (actions/runner, MIT) with
3+ small stubs for the assemblies they depend on, and a driver that
4+ evaluates expressions with them. See README.md. -->
5+ <PropertyGroup>
6+ <OutputType>Exe</OutputType>
7+ <TargetFramework>net9.0</TargetFramework>
8+ <Nullable>disable</Nullable>
9+ <LangVersion>11.0</LangVersion>
10+ <NoWarn>NU1701;CS8632;CS0108;CS0114;CS1591;CS0618;SYSLIB0050</NoWarn>
11+ <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
12+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
13+ <RunnerSrc Condition="'$(RunnerSrc)' == ''">$(HOME)/dev/clones/runner/src/Sdk</RunnerSrc>
14+ </PropertyGroup>
15+ <ItemGroup>
16+ <Compile Include="$(RunnerSrc)/DTExpressions2/**/*.cs" />
17+ <Compile Include="$(RunnerSrc)/DTLogging/**/*.cs" />
18+ <Compile Include="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/*.cs" Exclude="$(RunnerSrc)/DTPipelines/Pipelines/ContextData/PipelineContextDataExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateMemoryExtensions.cs;$(RunnerSrc)/DTPipelines/Pipelines/ContextData/TemplateTokenExtensions.cs" />
19+ <Compile Include="Stubs.cs" />
20+ <Compile Include="Program.cs" />
21+ <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
22+ </ItemGroup>
23+</Project>
A expr/testdata/runner.json.gz +0 -0 binary file: no text diff

binary file: no text diff

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