diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index e60bb50ee..3004e6802 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -73,6 +73,11 @@ namespace GitHub.Runner.Worker // Clear the cache (for self-hosted runners) IOUtil.DeleteDirectory(HostContext.GetDirectory(WellKnownDirectory.Actions), executionContext.CancellationToken); + // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed + var newActionMetadata = executionContext.Variables.GetBoolean("DistributedTask.NewActionMetadata") ?? false; + + var repositoryActions = new List(); + foreach (var action in actions) { if (action.Reference.Type == Pipelines.ActionSourceType.ContainerRegistry) @@ -90,7 +95,8 @@ namespace GitHub.Runner.Worker Trace.Info($"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'"); imagesToPull[containerReference.Image].Add(action.Id); } - else if (action.Reference.Type == Pipelines.ActionSourceType.Repository) + // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed + else if (action.Reference.Type == Pipelines.ActionSourceType.Repository && !newActionMetadata) { // only download the repository archive await DownloadRepositoryActionAsync(executionContext, action); @@ -124,6 +130,81 @@ namespace GitHub.Runner.Worker } } + var repoAction = action.Reference as Pipelines.RepositoryPathReference; + if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) + { + var definition = LoadAction(executionContext, action); + if (definition.Data.Execution.HasPre) + { + var actionRunner = HostContext.CreateService(); + actionRunner.Action = action; + actionRunner.Stage = ActionRunStage.Pre; + actionRunner.Condition = definition.Data.Execution.InitCondition; + + Trace.Info($"Add 'pre' execution for {action.Id}"); + preStepTracker[action.Id] = actionRunner; + } + } + } + else if (action.Reference.Type == Pipelines.ActionSourceType.Repository && newActionMetadata) + { + repositoryActions.Add(action); + } + } + + if (repositoryActions.Count > 0) + { + // Get the download info + var downloadInfos = await GetDownloadInfoAsync(executionContext, repositoryActions); + + // Download each action + foreach (var action in repositoryActions) + { + var lookupKey = GetDownloadInfoLookupKey(action); + if (string.IsNullOrEmpty(lookupKey)) + { + continue; + } + + if (!downloadInfos.TryGetValue(lookupKey, out var downloadInfo)) + { + throw new Exception($"Missing download info for {lookupKey}"); + } + + await DownloadRepositoryActionAsync(executionContext, downloadInfo); + } + + // More preparation based on content in the repository (action.yml) + foreach (var action in repositoryActions) + { + var setupInfo = PrepareRepositoryActionAsync(executionContext, action); + if (setupInfo != null) + { + if (!string.IsNullOrEmpty(setupInfo.Image)) + { + if (!imagesToPull.ContainsKey(setupInfo.Image)) + { + imagesToPull[setupInfo.Image] = new List(); + } + + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to pull image '{setupInfo.Image}'"); + imagesToPull[setupInfo.Image].Add(action.Id); + } + else + { + ArgUtil.NotNullOrEmpty(setupInfo.ActionRepository, nameof(setupInfo.ActionRepository)); + + if (!imagesToBuild.ContainsKey(setupInfo.ActionRepository)) + { + imagesToBuild[setupInfo.ActionRepository] = new List(); + } + + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.ActionRepository}' needs to build image '{setupInfo.Dockerfile}'"); + imagesToBuild[setupInfo.ActionRepository].Add(action.Id); + imagesToBuildInfo[setupInfo.ActionRepository] = setupInfo; + } + } + var repoAction = action.Reference as Pipelines.RepositoryPathReference; if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) { @@ -464,6 +545,127 @@ namespace GitHub.Runner.Worker } } + // This implementation is temporary and will be removed when we switch to a REST API call to the service to resolve the download info + private async Task RepoExistsAsync(IExecutionContext executionContext, Pipelines.RepositoryPathReference repositoryReference, string authorization) + { + var apiUrl = GetApiUrl(executionContext); + var repoUrl = $"{apiUrl}/repos/{repositoryReference.Name}"; + for (var attempt = 1; attempt <= 3; attempt++) + { + executionContext.Debug($"Checking whether repo exists: {repoUrl}"); + try + { + using (var httpClientHandler = HostContext.CreateHttpClientHandler()) + using (var httpClient = new HttpClient(httpClientHandler)) + { + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(authorization); + httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); + using (var response = await httpClient.GetAsync(repoUrl)) + { + if (response.IsSuccessStatusCode) + { + return true; + } + else if (response.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + else + { + // Throw + response.EnsureSuccessStatusCode(); + } + } + } + } + catch (Exception ex) + { + if (attempt < 3) + { + executionContext.Debug($"Failed checking whether repo '{repositoryReference.Name}' exists: {ex.Message}"); + } + else + { + executionContext.Error($"Failed checking whether repo '{repositoryReference.Name}' exists: {ex.Message}"); + throw; + } + } + } + + return false; // Never reaches here + } + + // This implementation is temporary and will be replaced with a REST API call to the service to resolve + private async Task> GetDownloadInfoAsync(IExecutionContext executionContext, List actions) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var configurationStore = HostContext.GetService(); + var runnerSettings = configurationStore.GetSettings(); + var apiUrl = GetApiUrl(executionContext); + var accessToken = executionContext.GetGitHubContext("token"); + var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{accessToken}")); + var authorization = $"Basic {base64EncodingToken}"; + + foreach (var action in actions) + { + var lookupKey = GetDownloadInfoLookupKey(action); + if (string.IsNullOrEmpty(lookupKey) || result.ContainsKey(lookupKey)) + { + continue; + } + + var repositoryReference = action.Reference as Pipelines.RepositoryPathReference; + ArgUtil.NotNull(repositoryReference, nameof(repositoryReference)); + + var downloadInfo = default(ActionDownloadInfo); + + if (runnerSettings.IsHostedServer) + { + downloadInfo = new ActionDownloadInfo + { + NameWithOwner = repositoryReference.Name, + Ref = repositoryReference.Ref, + ArchiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref), + Authorization = authorization, + }; + } + // Test whether the repo exists in the instance + else if (await RepoExistsAsync(executionContext, repositoryReference, authorization)) + { + downloadInfo = new ActionDownloadInfo + { + NameWithOwner = repositoryReference.Name, + Ref = repositoryReference.Ref, + ArchiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref), + Authorization = authorization, + }; + } + // Fallback to dotcom + else + { + downloadInfo = new ActionDownloadInfo + { + NameWithOwner = repositoryReference.Name, + Ref = repositoryReference.Ref, + ArchiveLink = BuildLinkToActionArchive(_dotcomApiUrl, repositoryReference.Name, repositoryReference.Ref), + Authorization = null, + }; + } + + result.Add(lookupKey, downloadInfo); + } + + // Register secrets + foreach (var downloadInfo in result.Values) + { + HostContext.SecretMasker.AddValue(downloadInfo.Authorization); + } + + return result; + } + + // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, Pipelines.ActionStep repositoryAction) { Trace.Entering(); @@ -509,7 +711,7 @@ namespace GitHub.Runner.Worker string archiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref); Trace.Info($"Download archive '{archiveLink}' to '{destDirectory}'."); var downloadDetails = new ActionDownloadDetails(archiveLink, ConfigureAuthorizationFromContext); - await DownloadRepositoryActionAsync(executionContext, downloadDetails, destDirectory); + await DownloadRepositoryActionAsync(executionContext, downloadDetails, null, destDirectory); return; } else @@ -536,7 +738,7 @@ namespace GitHub.Runner.Worker Trace.Info($"Download archive '{downloadAttempt.ArchiveLink}' to '{destDirectory}'."); try { - await DownloadRepositoryActionAsync(executionContext, downloadAttempt, destDirectory); + await DownloadRepositoryActionAsync(executionContext, downloadAttempt, null, destDirectory); return; } catch (ActionNotFoundException) @@ -549,6 +751,33 @@ namespace GitHub.Runner.Worker } } + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadInfo downloadInfo) + { + Trace.Entering(); + ArgUtil.NotNull(executionContext, nameof(executionContext)); + ArgUtil.NotNull(downloadInfo, nameof(downloadInfo)); + ArgUtil.NotNullOrEmpty(downloadInfo.NameWithOwner, nameof(downloadInfo.NameWithOwner)); + ArgUtil.NotNullOrEmpty(downloadInfo.Ref, nameof(downloadInfo.Ref)); + + string destDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), downloadInfo.NameWithOwner.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar), downloadInfo.Ref); + string watermarkFile = GetWatermarkFilePath(destDirectory); + if (File.Exists(watermarkFile)) + { + executionContext.Debug($"Action '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}' already downloaded at '{destDirectory}'."); + return; + } + else + { + // make sure we get a clean folder ready to use. + IOUtil.DeleteDirectory(destDirectory, executionContext.CancellationToken); + Directory.CreateDirectory(destDirectory); + executionContext.Output($"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'"); + } + + Trace.Info($"Download archive '{downloadInfo.ArchiveLink}' to '{destDirectory}'."); + await DownloadRepositoryActionAsync(executionContext, null, downloadInfo, destDirectory); + } + private string GetApiUrl(IExecutionContext executionContext) { string apiUrl = executionContext.GetGitHubContext("api_url"); @@ -569,7 +798,8 @@ namespace GitHub.Runner.Worker #endif } - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadDetails actionDownloadDetails, string destDirectory) + // todo: Remove the parameter "actionDownloadDetails" when feature flag DistributedTask.NewActionMetadata is removed + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadDetails actionDownloadDetails, ActionDownloadInfo downloadInfo, string destDirectory) { //download and extract action in a temp folder and rename it on success string tempDirectory = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Actions), "_temp_" + Guid.NewGuid()); @@ -581,7 +811,7 @@ namespace GitHub.Runner.Worker string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz"); #endif - string link = actionDownloadDetails.ArchiveLink; + string link = downloadInfo != null ? downloadInfo.ArchiveLink : actionDownloadDetails.ArchiveLink; Trace.Info($"Save archive '{link}' into {archiveFile}."); try { @@ -601,7 +831,16 @@ namespace GitHub.Runner.Worker using (var httpClientHandler = HostContext.CreateHttpClientHandler()) using (var httpClient = new HttpClient(httpClientHandler)) { - actionDownloadDetails.ConfigureAuthorization(executionContext, httpClient); + // Legacy + if (downloadInfo == null) + { + actionDownloadDetails.ConfigureAuthorization(executionContext, httpClient); + } + // FF DistributedTask.NewActionMetadata + else + { + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadInfo.Authorization); + } httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); using (var response = await httpClient.GetAsync(link)) @@ -741,6 +980,7 @@ namespace GitHub.Runner.Worker } } + // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed private void ConfigureAuthorizationFromContext(IExecutionContext executionContext, HttpClient httpClient) { var authToken = Environment.GetEnvironmentVariable("_GITHUB_ACTION_TOKEN"); @@ -872,6 +1112,48 @@ namespace GitHub.Runner.Worker } } + private static string GetDownloadInfoLookupKey(Pipelines.ActionStep action) + { + if (action.Reference.Type != Pipelines.ActionSourceType.Repository) + { + return null; + } + + var repositoryReference = action.Reference as Pipelines.RepositoryPathReference; + ArgUtil.NotNull(repositoryReference, nameof(repositoryReference)); + + if (string.Equals(repositoryReference.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + if (!string.Equals(repositoryReference.RepositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase)) + { + throw new NotSupportedException(repositoryReference.RepositoryType); + } + + ArgUtil.NotNullOrEmpty(repositoryReference.Name, nameof(repositoryReference.Name)); + ArgUtil.NotNullOrEmpty(repositoryReference.Ref, nameof(repositoryReference.Ref)); + return $"{repositoryReference.Name}@{repositoryReference.Ref}"; + } + + private static AuthenticationHeaderValue CreateAuthHeader(string authorization) + { + if (string.IsNullOrEmpty(authorization)) + { + return null; + } + + var split = authorization.Split(new char[] { ' ' }, 2); + if (split.Length != 2 || string.IsNullOrWhiteSpace(split[0]) || string.IsNullOrWhiteSpace(split[1])) + { + throw new Exception("Unexpected authorization header format"); + } + + return new AuthenticationHeaderValue(split[0].Trim(), split[1].Trim()); + } + + // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed private class ActionDownloadDetails { public string ArchiveLink { get; } @@ -884,6 +1166,17 @@ namespace GitHub.Runner.Worker ConfigureAuthorization = configureAuthorization; } } + + private class ActionDownloadInfo + { + public string NameWithOwner { get; set; } + + public string Ref { get; set; } + + public string ArchiveLink { get; set; } + + public string Authorization { get; set; } + } } public sealed class Definition diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 5be09a0ef..fb20b2356 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -33,6 +33,1803 @@ namespace GitHub.Runner.Common.Tests.Worker private ActionManager _actionManager; private string _workFolder; +#if OS_LINUX + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_PullImageFromDockerHub_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.ContainerRegistryReference() + { + Image = "ubuntu:16.04" + } + } + }; + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + //Assert + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal("ubuntu:16.04", (steps[0].Data as ContainerSetupInfo).Container.Image); + } + finally + { + Teardown(); + } + } +#endif + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DownloadActionFromGraph_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "actions/download-artifact", + Ref = "master", + RepositoryType = "GitHub" + } + } + }; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/download-artifact", "master.completed"); + Assert.True(File.Exists(watermarkFile)); + + var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "actions/download-artifact", "master", "action.yml"); + Assert.True(File.Exists(actionYamlFile)); + _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DownloadBuiltInActionFromGraph_OnPremises_Legacy() + { + try + { + // Arrange + Setup(newActionMetadata: false); + const string ActionName = "actions/sample-action"; + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = ActionName, + Ref = "master", + RepositoryType = "GitHub" + } + } + }; + + // Return a valid action from GHES via mock + const string ApiUrl = "https://ghes.example.com/api/v3"; + string expectedArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); + string archiveFile = await CreateRepoArchive(); + using var stream = File.OpenRead(archiveFile); + var mockClientHandler = new Mock(); + mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(expectedArchiveLink)), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) }); + + var mockHandlerFactory = new Mock(); + mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); + _hc.SetSingleton(mockHandlerFactory.Object); + + _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); + _configurationStore.Object.GetSettings().IsHostedServer = false; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); + Assert.True(File.Exists(watermarkFile)); + + var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); + Assert.True(File.Exists(actionYamlFile)); + _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DownloadActionFromDotCom_OnPremises_Legacy() + { + try + { + // Arrange + Setup(newActionMetadata: false); + const string ActionName = "ownerName/sample-action"; + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = ActionName, + Ref = "master", + RepositoryType = "GitHub" + } + } + }; + + // Return a valid action from GHES via mock + const string ApiUrl = "https://ghes.example.com/api/v3"; + string builtInArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); + string dotcomArchiveLink = GetLinkToActionArchive("https://api.github.com", ActionName, "master"); + string archiveFile = await CreateRepoArchive(); + using var stream = File.OpenRead(archiveFile); + var mockClientHandler = new Mock(); + mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(builtInArchiveLink)), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(dotcomArchiveLink)), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) }); + + var mockHandlerFactory = new Mock(); + mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); + _hc.SetSingleton(mockHandlerFactory.Object); + + _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); + _configurationStore.Object.GetSettings().IsHostedServer = false; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); + Assert.True(File.Exists(watermarkFile)); + + var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); + Assert.True(File.Exists(actionYamlFile)); + _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DownloadUnknownActionFromGraph_OnPremises_Legacy() + { + try + { + // Arrange + Setup(newActionMetadata: false); + const string ActionName = "ownerName/sample-action"; + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = ActionName, + Ref = "master", + RepositoryType = "GitHub" + } + } + }; + + // Return a valid action from GHES via mock + const string ApiUrl = "https://ghes.example.com/api/v3"; + string archiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); + string archiveFile = await CreateRepoArchive(); + using var stream = File.OpenRead(archiveFile); + var mockClientHandler = new Mock(); + mockClientHandler.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); + + var mockHandlerFactory = new Mock(); + mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); + _hc.SetSingleton(mockHandlerFactory.Object); + + _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); + _configurationStore.Object.GetSettings().IsHostedServer = false; + + //Act + Func action = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + await Assert.ThrowsAsync(action); + + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); + Assert.False(File.Exists(watermarkFile)); + + var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); + Assert.False(File.Exists(actionYamlFile)); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_AlwaysClearActionsCache_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List(); + + var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "notexist/no", "notexist.completed"); + Directory.CreateDirectory(Path.GetDirectoryName(watermarkFile)); + File.WriteAllText(watermarkFile, DateTime.UtcNow.ToString()); + Directory.CreateDirectory(Path.Combine(Path.GetDirectoryName(watermarkFile), "notexist")); + File.Copy(Path.Combine(TestUtil.GetSrcPath(), "Test", TestDataFolderName, "dockerfileaction.yml"), Path.Combine(Path.GetDirectoryName(watermarkFile), "notexist", "action.yml")); + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + // Make sure _actions folder get deleted + Assert.False(Directory.Exists(_hc.GetDirectory(WellKnownDirectory.Actions))); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_SkipDownloadActionForSelfRepo_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Path = "action", + RepositoryType = Pipelines.PipelineConstants.SelfAlias + } + } + }; + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.True(steps.Count == 0); + } + finally + { + Teardown(); + } + } + +#if OS_LINUX + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithDockerfile_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfile", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfile"); + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithDockerfileInRelativePath_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfileinrelativepath", + Path = "images/cli", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "images/cli", "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionfile_Dockerfile_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfileinrelativepath", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionfile_DockerfileRelativePath_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithActionfile_DockerfileRelativePath", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionfile_DockerfileRelativePath"); + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "images/Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionfile_DockerHubImage_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithActionfile_DockerHubImage", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "RepositoryActionWithActionfile_DockerHubImage"); + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal("ubuntu:18.04", (steps[0].Data as ContainerSetupInfo).Container.Image); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionYamlFile_DockerHubImage_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + 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)).ContainerSetupSteps; + + 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")] + public async void PrepareActions_RepositoryActionWithActionfileAndDockerfile_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithactionfileanddockerfile", + RepositoryType = "GitHub" + } + } + }; + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithactionfileanddockerfile"); + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + Assert.Equal(actionId, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[0].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[0].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_NotPullOrBuildImagesMultipleTimes_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId1 = Guid.NewGuid(); + var actionId2 = Guid.NewGuid(); + var actionId3 = Guid.NewGuid(); + var actionId4 = Guid.NewGuid(); + var actionId5 = Guid.NewGuid(); + var actionId6 = Guid.NewGuid(); + var actionId7 = Guid.NewGuid(); + var actionId8 = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId1, + Reference = new Pipelines.ContainerRegistryReference() + { + Image = "ubuntu:16.04" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId2, + Reference = new Pipelines.ContainerRegistryReference() + { + Image = "ubuntu:18.04" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId3, + Reference = new Pipelines.ContainerRegistryReference() + { + Image = "ubuntu:18.04" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId4, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "notpullorbuildimagesmultipletimes1", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId5, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfile", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId6, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfileinrelativepath", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId7, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfileinrelativepath", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId8, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "repositoryactionwithdockerfileinrelativepath", + Path = "images/cli", + RepositoryType = "GitHub" + } + } + }; + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + //Assert + Assert.Equal(actionId1, (steps[0].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal("ubuntu:16.04", (steps[0].Data as ContainerSetupInfo).Container.Image); + + Assert.Contains(actionId2, (steps[1].Data as ContainerSetupInfo).StepIds); + Assert.Contains(actionId3, (steps[1].Data as ContainerSetupInfo).StepIds); + Assert.Contains(actionId4, (steps[1].Data as ContainerSetupInfo).StepIds); + Assert.Equal("ubuntu:18.04", (steps[1].Data as ContainerSetupInfo).Container.Image); + + var actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfile"); + + Assert.Equal(actionId5, (steps[2].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[2].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[2].Data as ContainerSetupInfo).Container.Dockerfile); + + actionDir = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang", "runner_L0", "repositoryactionwithdockerfileinrelativepath"); + + Assert.Contains(actionId6, (steps[3].Data as ContainerSetupInfo).StepIds); + Assert.Contains(actionId7, (steps[3].Data as ContainerSetupInfo).StepIds); + Assert.Equal(actionDir, (steps[3].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "Dockerfile"), (steps[3].Data as ContainerSetupInfo).Container.Dockerfile); + + Assert.Equal(actionId8, (steps[4].Data as ContainerSetupInfo).StepIds[0]); + Assert.Equal(actionDir, (steps[4].Data as ContainerSetupInfo).Container.WorkingDirectory); + Assert.Equal(Path.Combine(actionDir, "images/cli", "Dockerfile"), (steps[4].Data as ContainerSetupInfo).Container.Dockerfile); + } + finally + { + Teardown(); + } + } +#endif + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithActionfile_Node_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "actions/setup-node", + Ref = "v1", + RepositoryType = "GitHub" + } + } + }; + + //Act + var steps = (await _actionManager.PrepareActionsAsync(_ec.Object, actions)).ContainerSetupSteps; + + // node.js based action doesn't need any extra steps to build/pull containers. + Assert.True(steps.Count == 0); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithInvalidWrapperActionfile_Node_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithInvalidWrapperActionfile_Node", + RepositoryType = "GitHub" + } + } + }; + + //Act + try + { + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + } + catch (ArgumentException) + { + var traceFile = Path.GetTempFileName(); + File.Copy(_hc.TraceFileName, traceFile, true); + Assert.Contains("Entry javascript file is not provided.", File.ReadAllText(traceFile)); + } + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_RepositoryActionWithWrapperActionfile_PreSteps_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + var actionId1 = Guid.NewGuid(); + var actionId2 = Guid.NewGuid(); + _hc.GetTrace().Info(actionId1); + _hc.GetTrace().Info(actionId2); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action1", + Id = actionId1, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Node", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action2", + Id = actionId2, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Docker", + RepositoryType = "GitHub" + } + } + }; + + //Act + var preResult = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + Assert.Equal(2, preResult.PreStepTracker.Count); + Assert.NotNull(preResult.PreStepTracker[actionId1]); + Assert.NotNull(preResult.PreStepTracker[actionId2]); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsContainerRegistryActionDefinition_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + + Pipelines.ActionStep instance = new Pipelines.ActionStep() + { + Id = Guid.NewGuid(), + Reference = new Pipelines.ContainerRegistryReference() + { + Image = "ubuntu:16.04" + } + }; + + _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "ubuntu:16.04" }; + + // Act. + Definition definition = _actionManager.LoadAction(_ec.Object, instance); + + // Assert. + Assert.NotNull(definition); + Assert.NotNull(definition.Data); + Assert.Equal("ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.True(string.IsNullOrEmpty((definition.Data.Execution as ContainerActionExecutionData).EntryPoint)); + Assert.Null((definition.Data.Execution as ContainerActionExecutionData).Arguments); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsScriptActionDefinition_Legacy() + { + try + { + //Arrange + Setup(newActionMetadata: false); + + Pipelines.ActionStep instance = new Pipelines.ActionStep() + { + Id = Guid.NewGuid(), + Reference = new Pipelines.ScriptReference() + }; + + // Act. + Definition definition = _actionManager.LoadAction(_ec.Object, instance); + + // Assert. + Assert.NotNull(definition); + Assert.NotNull(definition.Data); + Assert.True(definition.Data.Execution.ExecutionType == ActionExecutionType.Script); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsContainerActionDefinitionDockerfile_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + // Prepare the task.json content. + 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: 'docker' + image: 'Dockerfile' + args: + - '${{ inputs.greeting }}' + entrypoint: 'main.sh' + env: + Token: foo + Url: bar +"; + Pipelines.ActionStep instance; + string directory; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "image:1234" }; + + // 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 ContainerActionExecutionData)); // execution.Node + Assert.Equal("image:1234", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); + + foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) + { + Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); + } + + foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) + { + var key = env.Key.AssertString("key").Value; + if (key == "Token") + { + Assert.Equal("foo", env.Value.AssertString("value").Value); + } + else if (key == "Url") + { + Assert.Equal("bar", env.Value.AssertString("value").Value); + } + else + { + throw new NotSupportedException(key); + } + } + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsContainerActionDefinitionRegistry_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + // Prepare the task.json content. + 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: 'docker' + image: 'docker://ubuntu:16.04' + args: + - '${{ inputs.greeting }}' + entrypoint: 'main.sh' + env: + Token: foo + Url: ${{inputs.greeting}} +"; + Pipelines.ActionStep instance; + string directory; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + + _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "ubuntu:16.04" }; + // 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 ContainerActionExecutionData)); + Assert.Equal("ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); + + foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) + { + Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); + } + + foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) + { + var key = env.Key.AssertString("key").Value; + if (key == "Token") + { + Assert.Equal("foo", env.Value.AssertString("value").Value); + } + else if (key == "Url") + { + Assert.Equal("${{ inputs.greeting }}", env.Value.AssertScalar("value").ToString()); + } + else + { + throw new NotSupportedException(key); + } + } + + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsNodeActionDefinition_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + 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; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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")] + public void LoadsNodeActionDefinitionYaml_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + 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")] + public void LoadsContainerActionDefinitionDockerfile_SelfRepo_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + // Prepare the task.json content. + 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: 'docker' + image: 'Dockerfile' + args: + - '${{ inputs.greeting }}' + entrypoint: 'main.sh' + env: + Token: foo + Url: bar +"; + Pipelines.ActionStep instance; + string directory; + CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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 ContainerActionExecutionData)); // execution.Node + Assert.Equal("Dockerfile", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); + + foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) + { + Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); + } + + foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) + { + var key = env.Key.AssertString("key").Value; + if (key == "Token") + { + Assert.Equal("foo", env.Value.AssertString("value").Value); + } + else if (key == "Url") + { + Assert.Equal("bar", env.Value.AssertString("value").Value); + } + else + { + throw new NotSupportedException(key); + } + } + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsContainerActionDefinitionRegistry_SelfRepo_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + // Prepare the task.json content. + 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: 'docker' + image: 'docker://ubuntu:16.04' + args: + - '${{ inputs.greeting }}' + entrypoint: 'main.sh' + env: + Token: foo + Url: ${{inputs.greeting}} +"; + Pipelines.ActionStep instance; + string directory; + CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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 ContainerActionExecutionData)); + Assert.Equal("docker://ubuntu:16.04", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); + + foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) + { + Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); + } + + foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) + { + var key = env.Key.AssertString("key").Value; + if (key == "Token") + { + Assert.Equal("foo", env.Value.AssertString("value").Value); + } + else if (key == "Url") + { + Assert.Equal("${{ inputs.greeting }}", env.Value.AssertScalar("value").ToString()); + } + else + { + throw new NotSupportedException(key); + } + } + + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsNodeActionDefinition_SelfRepo_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + 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; + CreateSelfRepoAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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")] + public void LoadsNodeActionDefinition_Cleanup_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + 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' + post: 'cleanup.js' +"; + Pipelines.ActionStep instance; + string directory; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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); + Assert.Equal("cleanup.js", (definition.Data.Execution as NodeJSActionExecutionData).Post); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsContainerActionDefinitionDockerfile_Cleanup_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + // Prepare the task.json content. + 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: 'docker' + image: 'Dockerfile' + args: + - '${{ inputs.greeting }}' + entrypoint: 'main.sh' + env: + Token: foo + Url: bar + post-entrypoint: 'cleanup.sh' +"; + Pipelines.ActionStep instance; + string directory; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + _actionManager.CachedActionContainers[instance.Id] = new ContainerInfo() { ContainerImage = "image:1234" }; + + // 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 ContainerActionExecutionData)); // execution.Node + Assert.Equal("image:1234", (definition.Data.Execution as ContainerActionExecutionData).Image); + Assert.Equal("main.sh", (definition.Data.Execution as ContainerActionExecutionData).EntryPoint); + Assert.Equal("cleanup.sh", (definition.Data.Execution as ContainerActionExecutionData).Post); + + foreach (var arg in (definition.Data.Execution as ContainerActionExecutionData).Arguments) + { + Assert.Equal("${{ inputs.greeting }}", arg.AssertScalar("arg").ToString()); + } + + foreach (var env in (definition.Data.Execution as ContainerActionExecutionData).Environment) + { + var key = env.Key.AssertString("key").Value; + if (key == "Token") + { + Assert.Equal("foo", env.Value.AssertString("value").Value); + } + else if (key == "Url") + { + Assert.Equal("bar", env.Value.AssertString("value").Value); + } + else + { + throw new NotSupportedException(key); + } + } + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void LoadsPluginActionDefinition_Legacy() + { + try + { + // Arrange. + Setup(newActionMetadata: false); + const string Content = @" +name: 'Hello World' +description: 'Greet the world and record the time' +author: 'Test Corporation' +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: + plugin: 'someplugin' +"; + Pipelines.ActionStep instance; + string directory; + CreateAction(yamlContent: Content, instance: out instance, directory: out directory); + + // 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 PluginActionExecutionData)); + Assert.Equal("plugin.class, plugin", (definition.Data.Execution as PluginActionExecutionData).Plugin); + Assert.Equal("plugin.cleanup, plugin", (definition.Data.Execution as PluginActionExecutionData).Post); + } + finally + { + Teardown(); + } + } + #if OS_LINUX [Fact] [Trait("Level", "L0")] @@ -43,6 +1840,7 @@ namespace GitHub.Runner.Common.Tests.Worker { //Arrange Setup(); + // _ec.Variables. var actionId = Guid.NewGuid(); var actions = new List { @@ -113,184 +1911,6 @@ namespace GitHub.Runner.Common.Tests.Worker } } - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_DownloadBuiltInActionFromGraph_OnPremises() - { - try - { - // Arrange - Setup(); - const string ActionName = "actions/sample-action"; - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = Guid.NewGuid(), - Reference = new Pipelines.RepositoryPathReference() - { - Name = ActionName, - Ref = "master", - RepositoryType = "GitHub" - } - } - }; - - // Return a valid action from GHES via mock - const string ApiUrl = "https://ghes.example.com/api/v3"; - string expectedArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); - string archiveFile = await CreateRepoArchive(); - using var stream = File.OpenRead(archiveFile); - var mockClientHandler = new Mock(); - mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(expectedArchiveLink)), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) }); - - var mockHandlerFactory = new Mock(); - mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); - _hc.SetSingleton(mockHandlerFactory.Object); - - _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); - _configurationStore.Object.GetSettings().IsHostedServer = false; - - //Act - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - //Assert - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); - Assert.True(File.Exists(watermarkFile)); - - var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); - Assert.True(File.Exists(actionYamlFile)); - _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_DownloadActionFromDotCom_OnPremises() - { - try - { - // Arrange - Setup(); - const string ActionName = "ownerName/sample-action"; - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = Guid.NewGuid(), - Reference = new Pipelines.RepositoryPathReference() - { - Name = ActionName, - Ref = "master", - RepositoryType = "GitHub" - } - } - }; - - // Return a valid action from GHES via mock - const string ApiUrl = "https://ghes.example.com/api/v3"; - string builtInArchiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); - string dotcomArchiveLink = GetLinkToActionArchive("https://api.github.com", ActionName, "master"); - string archiveFile = await CreateRepoArchive(); - using var stream = File.OpenRead(archiveFile); - var mockClientHandler = new Mock(); - mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(builtInArchiveLink)), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); - mockClientHandler.Protected().Setup>("SendAsync", ItExpr.Is(m => m.RequestUri == new Uri(dotcomArchiveLink)), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(stream) }); - - var mockHandlerFactory = new Mock(); - mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); - _hc.SetSingleton(mockHandlerFactory.Object); - - _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); - _configurationStore.Object.GetSettings().IsHostedServer = false; - - //Act - await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - //Assert - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); - Assert.True(File.Exists(watermarkFile)); - - var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); - Assert.True(File.Exists(actionYamlFile)); - _hc.GetTrace().Info(File.ReadAllText(actionYamlFile)); - } - finally - { - Teardown(); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public async void PrepareActions_DownloadUnknownActionFromGraph_OnPremises() - { - try - { - // Arrange - Setup(); - const string ActionName = "ownerName/sample-action"; - var actions = new List - { - new Pipelines.ActionStep() - { - Name = "action", - Id = Guid.NewGuid(), - Reference = new Pipelines.RepositoryPathReference() - { - Name = ActionName, - Ref = "master", - RepositoryType = "GitHub" - } - } - }; - - // Return a valid action from GHES via mock - const string ApiUrl = "https://ghes.example.com/api/v3"; - string archiveLink = GetLinkToActionArchive(ApiUrl, ActionName, "master"); - string archiveFile = await CreateRepoArchive(); - using var stream = File.OpenRead(archiveFile); - var mockClientHandler = new Mock(); - mockClientHandler.Protected().Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) - .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.NotFound)); - - var mockHandlerFactory = new Mock(); - mockHandlerFactory.Setup(p => p.CreateClientHandler(It.IsAny())).Returns(mockClientHandler.Object); - _hc.SetSingleton(mockHandlerFactory.Object); - - _ec.Setup(x => x.GetGitHubContext("api_url")).Returns(ApiUrl); - _configurationStore.Object.GetSettings().IsHostedServer = false; - - //Act - Func action = async () => await _actionManager.PrepareActionsAsync(_ec.Object, actions); - - //Assert - await Assert.ThrowsAsync(action); - - var watermarkFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master.completed"); - Assert.False(File.Exists(watermarkFile)); - - var actionYamlFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), ActionName, "master", "action.yml"); - Assert.False(File.Exists(actionYamlFile)); - } - finally - { - Teardown(); - } - } - [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] @@ -1942,7 +3562,7 @@ runs: #endif } - private void Setup([CallerMemberName] string name = "") + private void Setup([CallerMemberName] string name = "", bool newActionMetadata = true) { _ecTokenSource?.Dispose(); _ecTokenSource = new CancellationTokenSource(); @@ -1955,7 +3575,12 @@ runs: _ec = new Mock(); _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token); - _ec.Setup(x => x.Variables).Returns(new Variables(_hc, new Dictionary())); + var variables = new Dictionary(); + if (newActionMetadata) + { + variables["DistributedTask.NewActionMetadata"] = "true"; + } + _ec.Setup(x => x.Variables).Returns(new Variables(_hc, variables)); _ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData()); _ec.Setup(x => x.ExpressionFunctions).Returns(new List()); _ec.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); });