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}