diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index c2d7d5396..7a19d6e38 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -162,7 +162,8 @@ namespace GitHub.Runner.Common public static class Path { public static readonly string ActionsDirectory = "_actions"; - public static readonly string ActionManifestFile = "action.yml"; + public static readonly string ActionManifestYmlFile = "action.yml"; + public static readonly string ActionManifestYamlFile = "action.yaml"; public static readonly string BinDirectory = "bin"; public static readonly string DiagDirectory = "_diag"; public static readonly string ExternalsDirectory = "externals"; diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index f4172f389..28ab95591 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -33,7 +33,7 @@ namespace GitHub.Runner.Worker { private const int _defaultFileStreamBufferSize = 4096; - //81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k). + //81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k). private const int _defaultCopyBufferSize = 81920; private readonly Dictionary _cachedActionContainers = new Dictionary(); @@ -198,14 +198,21 @@ namespace GitHub.Runner.Worker Trace.Info($"Load action that reference repository from '{actionDirectory}'"); definition.Directory = actionDirectory; - string manifestFile = Path.Combine(actionDirectory, "action.yml"); + string manifestFile = Path.Combine(actionDirectory, Constants.Path.ActionManifestYmlFile); + string manifestFileYaml = Path.Combine(actionDirectory, Constants.Path.ActionManifestYamlFile); string dockerFile = Path.Combine(actionDirectory, "Dockerfile"); string dockerFileLowerCase = Path.Combine(actionDirectory, "dockerfile"); - if (File.Exists(manifestFile)) + if (File.Exists(manifestFile) || File.Exists(manifestFileYaml)) { var manifestManager = HostContext.GetService(); - definition.Data = manifestManager.Load(executionContext, manifestFile); - + if (File.Exists(manifestFile)) + { + definition.Data = manifestManager.Load(executionContext, manifestFile); + } + else + { + definition.Data = manifestManager.Load(executionContext, manifestFileYaml); + } Trace.Verbose($"Action friendly name: '{definition.Data.Name}'"); Trace.Verbose($"Action description: '{definition.Data.Description}'"); @@ -314,7 +321,7 @@ namespace GitHub.Runner.Worker else { var fullPath = IOUtil.ResolvePath(actionDirectory, "."); // resolve full path without access filesystem. - throw new NotSupportedException($"Can't find 'action.yml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); + throw new NotSupportedException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); } } else if (action.Reference.Type == Pipelines.ActionSourceType.Script) @@ -477,7 +484,7 @@ namespace GitHub.Runner.Worker int retryCount = 0; - // Allow up to 20 * 60s for any action to be downloaded from github graph. + // Allow up to 20 * 60s for any action to be downloaded from github graph. int timeoutSeconds = 20 * 60; while (retryCount < 3) { @@ -655,12 +662,21 @@ namespace GitHub.Runner.Worker // find the docker file or action.yml file var dockerFile = Path.Combine(actionEntryDirectory, "Dockerfile"); var dockerFileLowerCase = Path.Combine(actionEntryDirectory, "dockerfile"); - var actionManifest = Path.Combine(actionEntryDirectory, "action.yml"); - if (File.Exists(actionManifest)) + var actionManifest = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYmlFile); + var actionManifestYaml = Path.Combine(actionEntryDirectory, Constants.Path.ActionManifestYamlFile); + if (File.Exists(actionManifest) || File.Exists(actionManifestYaml)) { executionContext.Debug($"action.yml for action: '{actionManifest}'."); var manifestManager = HostContext.GetService(); - var actionDefinitionData = manifestManager.Load(executionContext, actionManifest); + ActionDefinitionData actionDefinitionData = null; + if (File.Exists(actionManifest)) + { + actionDefinitionData = manifestManager.Load(executionContext, actionManifest); + } + else + { + actionDefinitionData = manifestManager.Load(executionContext, actionManifestYaml); + } if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Container) { @@ -720,7 +736,7 @@ namespace GitHub.Runner.Worker else { var fullPath = IOUtil.ResolvePath(actionEntryDirectory, "."); // resolve full path without access filesystem. - throw new InvalidOperationException($"Can't find 'action.yml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); + throw new InvalidOperationException($"Can't find 'action.yml', 'action.yaml' or 'Dockerfile' under '{fullPath}'. Did you forget to run actions/checkout before running your local action?"); } } } diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 0f73d52a9..51e09bff1 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -373,6 +373,45 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionYamlFile_DockerHubImage() + { + try + { + //Arrange + Setup(); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithActionYamlFile_DockerHubImage", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionYamlFile_DockerHubImage"); + + //Act + var steps = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + Assert.Equal((steps[0].Data as ContainerSetupInfo).StepIds[0], actionId); + Assert.Equal("ubuntu:18.04", (steps[0].Data as ContainerSetupInfo).Container.Image); + } + finally + { + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -672,7 +711,7 @@ namespace GitHub.Runner.Common.Tests.Worker name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -772,7 +811,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -871,7 +910,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -925,6 +964,87 @@ runs: } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsNodeActionDefinitionYaml() + { + try + { + // Arrange. + Setup(); + const string Content = @" +# Container action +name: 'Hello World' +description: 'Greet the world and record the time' +author: 'GitHub' +inputs: + greeting: # id of input + description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' + required: true + default: 'Hello' + entryPoint: # id of input + description: 'optional docker entrypoint overwrite.' + required: false +outputs: + time: # id of output + description: 'The time we did the greeting' +icon: 'hello.svg' # vector art to display in the GitHub Marketplace +color: 'green' # optional, decorates the entry in the GitHub Marketplace +runs: + using: 'node12' + main: 'task.js' +"; + Pipelines.ActionStep instance; + string directory; + directory = Path.Combine(_workFolder, Constants.Path.ActionsDirectory, "GitHub/actions".Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), "master"); + string file = Path.Combine(directory, Constants.Path.ActionManifestYamlFile); + Directory.CreateDirectory(Path.GetDirectoryName(file)); + File.WriteAllText(file, Content); + instance = new Pipelines.ActionStep() + { + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "GitHub/actions", + Ref = "master", + RepositoryType = Pipelines.RepositoryTypes.GitHub + } + }; + + // Act. + Definition definition = _actionManager.LoadAction(_ec.Object, instance); + + // Assert. + Assert.NotNull(definition); + Assert.Equal(directory, definition.Directory); + Assert.NotNull(definition.Data); + Assert.NotNull(definition.Data.Inputs); // inputs + Dictionary inputDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var input in definition.Data.Inputs) + { + var name = input.Key.AssertString("key").Value; + var value = input.Value.AssertScalar("value").ToString(); + + _hc.GetTrace().Info($"Default: {name} = {value}"); + inputDefaults[name] = value; + } + + Assert.Equal(2, inputDefaults.Count); + Assert.True(inputDefaults.ContainsKey("greeting")); + Assert.Equal("Hello", inputDefaults["greeting"]); + Assert.True(string.IsNullOrEmpty(inputDefaults["entryPoint"])); + Assert.NotNull(definition.Data.Execution); // execution + + Assert.NotNull((definition.Data.Execution as NodeJSActionExecutionData)); + Assert.Equal("task.js", (definition.Data.Execution as NodeJSActionExecutionData).Script); + } + finally + { + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -940,7 +1060,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1039,7 +1159,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1137,7 +1257,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1205,7 +1325,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1276,7 +1396,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'GitHub' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1376,7 +1496,7 @@ runs: name: 'Hello World' description: 'Greet the world and record the time' author: 'Test Corporation' -inputs: +inputs: greeting: # id of input description: 'The greeting we choose - will print ""{greeting}, World!"" on stdout' required: true @@ -1433,7 +1553,7 @@ runs: private void CreateAction(string yamlContent, out Pipelines.ActionStep instance, out string directory) { directory = Path.Combine(_workFolder, Constants.Path.ActionsDirectory, "GitHub/actions".Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), "master"); - string file = Path.Combine(directory, Constants.Path.ActionManifestFile); + string file = Path.Combine(directory, Constants.Path.ActionManifestYmlFile); Directory.CreateDirectory(Path.GetDirectoryName(file)); File.WriteAllText(file, yamlContent); instance = new Pipelines.ActionStep() @@ -1451,7 +1571,7 @@ runs: private void CreateSelfRepoAction(string yamlContent, out Pipelines.ActionStep instance, out string directory) { directory = Path.Combine(_workFolder, "actions", "actions"); - string file = Path.Combine(directory, Constants.Path.ActionManifestFile); + string file = Path.Combine(directory, Constants.Path.ActionManifestYmlFile); Directory.CreateDirectory(Path.GetDirectoryName(file)); File.WriteAllText(file, yamlContent); instance = new Pipelines.ActionStep()