v0.2.1 98594d0

Driver.cs RawHistoryFolder

1// SPDX-License-Identifier: MIT
2//
3// Driver that runs the C# runner's action.yml loader on a directory of files.
4// It is compiled into a copy of actions/runner src/Sdk (see build.sh), so that
5// it can use the internal types the runner uses.
6//
7// Load, ConvertRuns and ConvertInputs below are
8// Runner.Worker/ActionManifestManagerLegacy.cs at the commit in
9// runner-commit.txt, with what needs an IExecutionContext removed (tracing,
10// the file table, expression values: none of it influences loading) and the
11// result written to JSON instead of ActionDefinitionData. Nothing else was
12// changed: the flow of exceptions and errors is the runner's, because it
13// decides which messages a user sees.
14//
15// Why the legacy loader: ActionManifestManagerWrapper.Load runs
16// ActionManifestManagerLegacy and returns its result; the newer
17// ActionManifestManager only runs next to it when the feature flag
18// actions_runner_compare_workflow_parser or the environment variable
19// ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER is set, and its result is discarded.
20
21using System;
22using System.Collections.Generic;
23using System.IO;
24using System.Linq;
25using GitHub.DistributedTask.ObjectTemplating;
26using GitHub.DistributedTask.ObjectTemplating.Schema;
27using GitHub.DistributedTask.ObjectTemplating.Tokens;
28using GitHub.DistributedTask.Pipelines;
29using GitHub.DistributedTask.Pipelines.ObjectTemplating;
30using Newtonsoft.Json;
31using Newtonsoft.Json.Linq;
32
33namespace ActionOracle
34{
35    public static class Program
36    {
37        public static int Main(string[] args)
38        {
39            if (args.Length != 2 && args.Length != 3)
40            {
41                Console.Error.WriteLine("usage: Sdk.dll <action_yaml.json> <dir with *.yml> [<file with one uses value per line>]");
42                return 2;
43            }
44
45            var schemaJson = File.ReadAllText(args[0]);
46            var schema = TemplateSchema.Load(new JsonObjectReader(null, schemaJson));
47            if (args.Length == 3)
48            {
49                // One-step composite action per "uses" value (with an id, so that
50                // a value of the wrong form is reported and nothing else fails):
51                // the answer is the error list and the reference the runner made.
52                var uses = new JObject();
53                foreach (var line in File.ReadAllLines(args[2]))
54                {
55                    var yaml = "name: a\ndescription: d\nruns:\n  using: composite\n  steps:\n    - uses: " + JsonConvert.ToString(line) + "\n      id: x\n";
56                    uses[line] = Load(schema, "u.yml", yaml);
57                }
58                Console.Out.Write(uses.ToString(Formatting.Indented));
59                Console.Out.WriteLine();
60                return 0;
61            }
62
63            var result = new JObject();
64            foreach (var file in Directory.GetFiles(args[1], "*.yml").OrderBy(x => x, StringComparer.Ordinal))
65            {
66                var name = Path.GetFileName(file);
67                result[name] = Load(schema, name, File.ReadAllText(file));
68            }
69
70            Console.Out.Write(result.ToString(Formatting.Indented));
71            Console.Out.WriteLine();
72            return 0;
73        }
74
75        static JObject Load(TemplateSchema schema, string fileRelativePath, string fileContent)
76        {
77            var templateContext = new TemplateContext
78            {
79                CancellationToken = System.Threading.CancellationToken.None,
80                Errors = new TemplateValidationErrors(10, int.MaxValue),
81                Memory = new TemplateMemory(maxDepth: 100, maxEvents: 1000000, maxBytes: 10 * 1024 * 1024),
82                Schema = schema,
83                TraceWriter = new EmptyTraceWriter(),
84            };
85
86            var summary = new JObject();
87            JObject execution = null;
88            var inputs = new JArray();
89            var deprecated = new JObject();
90
91            try
92            {
93                var token = default(TemplateToken);
94                var fileId = templateContext.GetFileId(fileRelativePath);
95                using (var stringReader = new StringReader(fileContent))
96                {
97                    var yamlObjectReader = new YamlObjectReader(fileId, stringReader);
98                    token = TemplateReader.Read(templateContext, "action-root", yamlObjectReader, fileId, out _);
99                }
100
101                var actionMapping = token.AssertMapping("action manifest root");
102                var actionOutputs = default(MappingToken);
103                var actionRunValueToken = default(TemplateToken);
104
105                foreach (var actionPair in actionMapping)
106                {
107                    var propertyName = actionPair.Key.AssertString($"action.yml property key");
108
109                    switch (propertyName.Value)
110                    {
111                        case "name":
112                            summary["name"] = actionPair.Value.AssertString("name").Value;
113                            break;
114                        case "outputs":
115                            actionOutputs = actionPair.Value.AssertMapping("outputs");
116                            break;
117                        case "description":
118                            summary["description"] = actionPair.Value.AssertString("description").Value;
119                            break;
120                        case "inputs":
121                            ConvertInputs(actionPair.Value, inputs, deprecated);
122                            break;
123                        case "runs":
124                            actionRunValueToken = actionPair.Value;
125                            break;
126                        default:
127                            break;
128                    }
129                }
130
131                if (actionRunValueToken != null)
132                {
133                    execution = ConvertRuns(templateContext, actionRunValueToken, fileRelativePath, actionOutputs);
134                }
135            }
136            catch (Exception ex)
137            {
138                templateContext.Errors.Add(ex);
139            }
140
141            var errors = new JArray();
142            foreach (var error in templateContext.Errors)
143            {
144                // ex.ToString() ends with the stack trace, which names temporary
145                // paths and line numbers of this driver: keep the message only.
146                var message = error.Message;
147                var at = message.IndexOf("\n   at ", StringComparison.Ordinal);
148                errors.Add(at >= 0 ? message.Substring(0, at) : message);
149            }
150
151            if (errors.Count > 0)
152            {
153                summary["errors"] = errors;
154                summary["failed"] = $"Failed to load {fileRelativePath}";
155                return summary;
156            }
157
158            if (execution == null)
159            {
160                summary["errors"] = new JArray($"Top level 'runs:' section is required for {fileRelativePath}");
161                return summary;
162            }
163
164            summary["inputs"] = inputs;
165            if (deprecated.Count > 0)
166            {
167                summary["deprecated"] = deprecated;
168            }
169            summary["execution"] = execution;
170            return summary;
171        }
172
173        static JObject ConvertRuns(
174            TemplateContext templateContext,
175            TemplateToken inputsToken,
176            String fileRelativePath,
177            MappingToken outputs = null)
178        {
179            var runsMapping = inputsToken.AssertMapping("runs");
180            var usingToken = default(StringToken);
181            var imageToken = default(StringToken);
182            var argsToken = default(SequenceToken);
183            var entrypointToken = default(StringToken);
184            var envToken = default(MappingToken);
185            var mainToken = default(StringToken);
186            var pluginToken = default(StringToken);
187            var preToken = default(StringToken);
188            var preEntrypointToken = default(StringToken);
189            var preIfToken = default(StringToken);
190            var postToken = default(StringToken);
191            var postEntrypointToken = default(StringToken);
192            var postIfToken = default(StringToken);
193            var steps = default(List<Step>);
194
195            foreach (var run in runsMapping)
196            {
197                var runsKey = run.Key.AssertString("runs key").Value;
198                switch (runsKey)
199                {
200                    case "using":
201                        usingToken = run.Value.AssertString("using");
202                        break;
203                    case "image":
204                        imageToken = run.Value.AssertString("image");
205                        break;
206                    case "args":
207                        argsToken = run.Value.AssertSequence("args");
208                        break;
209                    case "entrypoint":
210                        entrypointToken = run.Value.AssertString("entrypoint");
211                        break;
212                    case "env":
213                        envToken = run.Value.AssertMapping("env");
214                        break;
215                    case "main":
216                        mainToken = run.Value.AssertString("main");
217                        break;
218                    case "plugin":
219                        pluginToken = run.Value.AssertString("plugin");
220                        break;
221                    case "post":
222                        postToken = run.Value.AssertString("post");
223                        break;
224                    case "post-entrypoint":
225                        postEntrypointToken = run.Value.AssertString("post-entrypoint");
226                        break;
227                    case "post-if":
228                        postIfToken = run.Value.AssertString("post-if");
229                        break;
230                    case "pre":
231                        preToken = run.Value.AssertString("pre");
232                        break;
233                    case "pre-entrypoint":
234                        preEntrypointToken = run.Value.AssertString("pre-entrypoint");
235                        break;
236                    case "pre-if":
237                        preIfToken = run.Value.AssertString("pre-if");
238                        break;
239                    case "steps":
240                        var stepsToken = run.Value.AssertSequence("steps");
241                        steps = PipelineTemplateConverter.ConvertToSteps(templateContext, stepsToken);
242                        templateContext.Errors.Check();
243                        break;
244                    default:
245                        break;
246                }
247            }
248
249            if (usingToken != null)
250            {
251                if (string.Equals(usingToken.Value, "docker", StringComparison.OrdinalIgnoreCase))
252                {
253                    if (string.IsNullOrEmpty(imageToken?.Value))
254                    {
255                        throw new ArgumentNullException($"You are using a Container Action but an image is not provided in {fileRelativePath}.");
256                    }
257                    else
258                    {
259                        return new JObject
260                        {
261                            ["type"] = "docker",
262                            ["using"] = usingToken.Value,
263                            ["image"] = imageToken.Value,
264                            ["args"] = argsToken == null ? null : new JArray(argsToken.Select(x => x.ToString())),
265                            ["entrypoint"] = entrypointToken?.Value,
266                            ["env"] = envToken == null ? null : Pairs(envToken),
267                            ["pre"] = preEntrypointToken?.Value,
268                            ["preIf"] = preIfToken?.Value ?? "always()",
269                            ["post"] = postEntrypointToken?.Value,
270                            ["postIf"] = postIfToken?.Value ?? "always()",
271                        };
272                    }
273                }
274                else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) ||
275                         string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) ||
276                         string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) ||
277                         string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase))
278                {
279                    if (string.IsNullOrEmpty(mainToken?.Value))
280                    {
281                        throw new ArgumentNullException($"You are using a JavaScript Action but there is not an entry JavaScript file provided in {fileRelativePath}.");
282                    }
283                    else
284                    {
285                        return new JObject
286                        {
287                            ["type"] = "node",
288                            ["using"] = usingToken.Value,
289                            ["main"] = mainToken.Value,
290                            ["pre"] = preToken?.Value,
291                            ["preIf"] = preIfToken?.Value ?? "always()",
292                            ["post"] = postToken?.Value,
293                            ["postIf"] = postIfToken?.Value ?? "always()",
294                        };
295                    }
296                }
297                else if (string.Equals(usingToken.Value, "composite", StringComparison.OrdinalIgnoreCase))
298                {
299                    if (steps == null)
300                    {
301                        throw new ArgumentNullException($"You are using a composite action but there are no steps provided in {fileRelativePath}.");
302                    }
303                    else
304                    {
305                        return new JObject
306                        {
307                            ["type"] = "composite",
308                            ["using"] = usingToken.Value,
309                            ["steps"] = new JArray(steps.Cast<ActionStep>().Select(Summarize)),
310                            ["outputs"] = outputs == null ? null : new JArray(outputs.Select(x => new JArray(x.Key.ToString(), OutputValue(x.Value)))),
311                        };
312                    }
313                }
314                else
315                {
316                    throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead.");
317                }
318            }
319            else if (pluginToken != null)
320            {
321                return new JObject { ["type"] = "plugin", ["plugin"] = pluginToken.Value };
322            }
323
324            throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.");
325        }
326
327        static string OutputValue(TemplateToken value)
328        {
329            if (value is MappingToken m)
330            {
331                foreach (var p in m)
332                {
333                    if (p.Key.ToString() == "value") return p.Value.ToString();
334                }
335            }
336            return null;
337        }
338
339        static JObject Summarize(ActionStep a)
340        {
341            var o = new JObject { ["id"] = a.ContextName, ["if"] = a.Condition };
342            o["name"] = a.DisplayNameToken?.ToString();
343            o["continueOnError"] = a.ContinueOnError?.ToString();
344            o["timeout"] = a.TimeoutInMinutes?.ToString();
345            if (a.Environment is MappingToken)
346            {
347                o["env"] = Pairs(a.Environment);
348            }
349            else if (a.Environment != null)
350            {
351                o["envExpr"] = a.Environment.ToString();
352            }
353            if (a.Reference is ScriptReference)
354            {
355                o["kind"] = "run";
356                var inputs = (MappingToken)a.Inputs;
357                foreach (var p in inputs)
358                {
359                    o[p.Key.ToString()] = p.Value.ToString();
360                }
361            }
362            else
363            {
364                o["kind"] = "uses";
365                if (a.Inputs is MappingToken)
366                {
367                    o["with"] = Pairs(a.Inputs);
368                }
369                else if (a.Inputs != null)
370                {
371                    o["withExpr"] = a.Inputs.ToString();
372                }
373                if (a.Reference is ContainerRegistryReference c)
374                {
375                    o["reference"] = new JObject { ["type"] = "docker", ["image"] = c.Image };
376                }
377                else if (a.Reference is RepositoryPathReference r)
378                {
379                    o["reference"] = new JObject { ["type"] = r.RepositoryType, ["name"] = r.Name, ["ref"] = r.Ref, ["path"] = r.Path };
380                }
381            }
382            return o;
383        }
384
385        static JArray Pairs(TemplateToken t)
386        {
387            return t is MappingToken m ? new JArray(m.Select(x => new JArray(x.Key.ToString(), x.Value.ToString()))) : new JArray(t.ToString());
388        }
389
390        static void ConvertInputs(TemplateToken inputsToken, JArray inputs, JObject deprecated)
391        {
392            var inputsMapping = inputsToken.AssertMapping("inputs");
393            foreach (var input in inputsMapping)
394            {
395                bool hasDefault = false;
396                var inputName = input.Key.AssertString("input name");
397                var inputMetadata = input.Value.AssertMapping("input metadata");
398                var entry = new JObject { ["name"] = inputName.Value };
399                foreach (var metadata in inputMetadata)
400                {
401                    var metadataName = metadata.Key.AssertString("input metadata").Value;
402                    if (string.Equals(metadataName, "default", StringComparison.OrdinalIgnoreCase))
403                    {
404                        hasDefault = true;
405                        entry["default"] = metadata.Value.ToString();
406                    }
407                    else if (string.Equals(metadataName, "deprecationMessage", StringComparison.OrdinalIgnoreCase))
408                    {
409                        var message = metadata.Value.AssertString("input deprecationMessage");
410                        deprecated.Add(inputName.Value, message.Value);
411                    }
412                }
413
414                if (!hasDefault)
415                {
416                    entry["default"] = string.Empty;
417                }
418                inputs.Add(entry);
419            }
420        }
421    }
422}

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