From 5815819f248b4f9c3fc45f7adfdf5d6b5725195f Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 9 Jun 2020 08:53:28 -0400 Subject: [PATCH] Resolve action download info (#515) --- src/Runner.Common/JobServer.cs | 10 + src/Runner.Worker/ActionManager.cs | 171 +++++++++--------- src/Runner.Worker/ExecutionContext.cs | 6 +- .../Generated/TaskHttpClientBase.cs | 32 ++++ src/Sdk/DTWebApi/WebApi/ActionDownloadInfo.cs | 40 ++++ .../WebApi/ActionDownloadInfoCollection.cs | 16 ++ src/Sdk/DTWebApi/WebApi/ActionReference.cs | 22 +++ .../DTWebApi/WebApi/ActionReferenceList.cs | 16 ++ src/Test/L0/Worker/ActionManagerL0.cs | 22 +++ 9 files changed, 252 insertions(+), 83 deletions(-) create mode 100644 src/Sdk/DTWebApi/WebApi/ActionDownloadInfo.cs create mode 100644 src/Sdk/DTWebApi/WebApi/ActionDownloadInfoCollection.cs create mode 100644 src/Sdk/DTWebApi/WebApi/ActionReference.cs create mode 100644 src/Sdk/DTWebApi/WebApi/ActionReferenceList.cs diff --git a/src/Runner.Common/JobServer.cs b/src/Runner.Common/JobServer.cs index 860555419..e3e0f551b 100644 --- a/src/Runner.Common/JobServer.cs +++ b/src/Runner.Common/JobServer.cs @@ -22,6 +22,7 @@ namespace GitHub.Runner.Common Task> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable records, CancellationToken cancellationToken); Task RaisePlanEventAsync(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent; Task GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken); + Task ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken); } public sealed class JobServer : RunnerService, IJobServer @@ -113,5 +114,14 @@ namespace GitHub.Runner.Common CheckConnection(); return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken); } + + //----------------------------------------------------------------- + // Action download info + //----------------------------------------------------------------- + public Task ResolveActionDownloadInfoAsync(Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken) + { + CheckConnection(); + return _taskClient.ResolveActionDownloadInfoAsync(scopeIdentifier, hubName, planId, actions, cancellationToken: cancellationToken); + } } } diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 3004e6802..347eb3d0d 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -14,6 +14,7 @@ using GitHub.Runner.Common; using GitHub.Runner.Sdk; using GitHub.Runner.Worker.Container; using GitHub.Services.Common; +using WebApi = GitHub.DistributedTask.WebApi; using Pipelines = GitHub.DistributedTask.Pipelines; using PipelineTemplateConstants = GitHub.DistributedTask.Pipelines.ObjectTemplating.PipelineTemplateConstants; @@ -546,10 +547,10 @@ 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) + private async Task RepoExistsAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo actionDownloadInfo, string token) { var apiUrl = GetApiUrl(executionContext); - var repoUrl = $"{apiUrl}/repos/{repositoryReference.Name}"; + var repoUrl = $"{apiUrl}/repos/{actionDownloadInfo.NameWithOwner}"; for (var attempt = 1; attempt <= 3; attempt++) { executionContext.Debug($"Checking whether repo exists: {repoUrl}"); @@ -558,7 +559,7 @@ namespace GitHub.Runner.Worker using (var httpClientHandler = HostContext.CreateHttpClientHandler()) using (var httpClient = new HttpClient(httpClientHandler)) { - httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(authorization); + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(token); httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); using (var response = await httpClient.GetAsync(repoUrl)) { @@ -582,11 +583,11 @@ namespace GitHub.Runner.Worker { if (attempt < 3) { - executionContext.Debug($"Failed checking whether repo '{repositoryReference.Name}' exists: {ex.Message}"); + executionContext.Debug($"Failed checking whether repo '{actionDownloadInfo.NameWithOwner}' exists: {ex.Message}"); } else { - executionContext.Error($"Failed checking whether repo '{repositoryReference.Name}' exists: {ex.Message}"); + executionContext.Error($"Failed checking whether repo '{actionDownloadInfo.NameWithOwner}' exists: {ex.Message}"); throw; } } @@ -596,73 +597,89 @@ namespace GitHub.Runner.Worker } // 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) + private async Task> GetDownloadInfoAsync(IExecutionContext executionContext, List actions) { - var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + executionContext.Output("Getting action download info"); + // Convert to action reference + var actionReferences = actions + .GroupBy(x => GetDownloadInfoLookupKey(x)) + .Where(x => !string.IsNullOrEmpty(x.Key)) + .Select(x => + { + var action = x.First(); + var repositoryReference = action.Reference as Pipelines.RepositoryPathReference; + ArgUtil.NotNull(repositoryReference, nameof(repositoryReference)); + return new WebApi.ActionReference + { + NameWithOwner = repositoryReference.Name, + Ref = repositoryReference.Ref, + }; + }) + .ToList(); + + // Nothing to resolve? + if (actionReferences.Count == 0) + { + return new Dictionary(); + } + + // Resolve download info + var jobServer = HostContext.GetService(); + var actionDownloadInfos = default(WebApi.ActionDownloadInfoCollection); + for (var attempt = 1; attempt <= 3; attempt++) + { + try + { + actionDownloadInfos = await jobServer.ResolveActionDownloadInfoAsync(executionContext.Plan.ScopeIdentifier, executionContext.Plan.PlanType, executionContext.Plan.PlanId, new WebApi.ActionReferenceList { Actions = actionReferences }, executionContext.CancellationToken); + break; + } + catch (Exception ex) when (attempt < 3) + { + executionContext.Output($"Failed to resolve action download info. Error: {ex.Message}"); + executionContext.Debug(ex.ToString()); + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GITHUB_ACTION_DOWNLOAD_NO_BACKOFF"))) + { + var backoff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)); + executionContext.Output($"Retrying in {backoff.TotalSeconds} seconds"); + await Task.Delay(backoff); + } + } + } + + ArgUtil.NotNull(actionDownloadInfos, nameof(actionDownloadInfos)); + ArgUtil.NotNull(actionDownloadInfos.Actions, nameof(actionDownloadInfos.Actions)); + var apiUrl = GetApiUrl(executionContext); + var defaultAccessToken = executionContext.GetGitHubContext("token"); 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) + foreach (var actionDownloadInfo in actionDownloadInfos.Actions.Values) { - 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); + // Add secret + HostContext.SecretMasker.AddValue(actionDownloadInfo.Authentication?.Token); + // Temporary code: Fix token and download URL if (runnerSettings.IsHostedServer) { - downloadInfo = new ActionDownloadInfo - { - NameWithOwner = repositoryReference.Name, - Ref = repositoryReference.Ref, - ArchiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref), - Authorization = authorization, - }; + actionDownloadInfo.Authentication = new WebApi.ActionDownloadAuthentication { Token = defaultAccessToken }; + actionDownloadInfo.TarballUrl = actionDownloadInfo.TarballUrl.Replace("", apiUrl); + actionDownloadInfo.ZipballUrl = actionDownloadInfo.ZipballUrl.Replace("", apiUrl); } - // Test whether the repo exists in the instance - else if (await RepoExistsAsync(executionContext, repositoryReference, authorization)) + else if (await RepoExistsAsync(executionContext, actionDownloadInfo, defaultAccessToken)) { - downloadInfo = new ActionDownloadInfo - { - NameWithOwner = repositoryReference.Name, - Ref = repositoryReference.Ref, - ArchiveLink = BuildLinkToActionArchive(apiUrl, repositoryReference.Name, repositoryReference.Ref), - Authorization = authorization, - }; + actionDownloadInfo.Authentication = new WebApi.ActionDownloadAuthentication { Token = defaultAccessToken }; + actionDownloadInfo.TarballUrl = actionDownloadInfo.TarballUrl.Replace("", apiUrl); + actionDownloadInfo.ZipballUrl = actionDownloadInfo.ZipballUrl.Replace("", apiUrl); } - // Fallback to dotcom else { - downloadInfo = new ActionDownloadInfo - { - NameWithOwner = repositoryReference.Name, - Ref = repositoryReference.Ref, - ArchiveLink = BuildLinkToActionArchive(_dotcomApiUrl, repositoryReference.Name, repositoryReference.Ref), - Authorization = null, - }; + actionDownloadInfo.TarballUrl = actionDownloadInfo.TarballUrl.Replace("", "https://api.github.com"); + actionDownloadInfo.ZipballUrl = actionDownloadInfo.ZipballUrl.Replace("", "https://api.github.com"); } - - result.Add(lookupKey, downloadInfo); } - // Register secrets - foreach (var downloadInfo in result.Values) - { - HostContext.SecretMasker.AddValue(downloadInfo.Authorization); - } - - return result; + return actionDownloadInfos.Actions; } // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed @@ -709,7 +726,6 @@ namespace GitHub.Runner.Worker { string apiUrl = GetApiUrl(executionContext); 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, null, destDirectory); return; @@ -735,7 +751,6 @@ namespace GitHub.Runner.Worker foreach (var downloadAttempt in downloadAttempts) { - Trace.Info($"Download archive '{downloadAttempt.ArchiveLink}' to '{destDirectory}'."); try { await DownloadRepositoryActionAsync(executionContext, downloadAttempt, null, destDirectory); @@ -751,7 +766,7 @@ namespace GitHub.Runner.Worker } } - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadInfo downloadInfo) + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo) { Trace.Entering(); ArgUtil.NotNull(executionContext, nameof(executionContext)); @@ -774,7 +789,6 @@ namespace GitHub.Runner.Worker executionContext.Output($"Download action repository '{downloadInfo.NameWithOwner}@{downloadInfo.Ref}'"); } - Trace.Info($"Download archive '{downloadInfo.ArchiveLink}' to '{destDirectory}'."); await DownloadRepositoryActionAsync(executionContext, null, downloadInfo, destDirectory); } @@ -799,7 +813,7 @@ namespace GitHub.Runner.Worker } // todo: Remove the parameter "actionDownloadDetails" when feature flag DistributedTask.NewActionMetadata is removed - private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadDetails actionDownloadDetails, ActionDownloadInfo downloadInfo, string destDirectory) + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, ActionDownloadDetails actionDownloadDetails, WebApi.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()); @@ -807,11 +821,12 @@ namespace GitHub.Runner.Worker #if OS_WINDOWS string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.zip"); + string link = downloadInfo?.ZipballUrl ?? actionDownloadDetails.ArchiveLink; #else string archiveFile = Path.Combine(tempDirectory, $"{Guid.NewGuid()}.tar.gz"); + string link = downloadInfo?.TarballUrl ?? actionDownloadDetails.ArchiveLink; #endif - string link = downloadInfo != null ? downloadInfo.ArchiveLink : actionDownloadDetails.ArchiveLink; Trace.Info($"Save archive '{link}' into {archiveFile}."); try { @@ -839,7 +854,7 @@ namespace GitHub.Runner.Worker // FF DistributedTask.NewActionMetadata else { - httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadInfo.Authorization); + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadInfo.Authentication?.Token); } httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); @@ -1137,20 +1152,23 @@ namespace GitHub.Runner.Worker return $"{repositoryReference.Name}@{repositoryReference.Ref}"; } - private static AuthenticationHeaderValue CreateAuthHeader(string authorization) + private static string GetDownloadInfoLookupKey(WebApi.ActionDownloadInfo info) { - if (string.IsNullOrEmpty(authorization)) + ArgUtil.NotNullOrEmpty(info.NameWithOwner, nameof(info.NameWithOwner)); + ArgUtil.NotNullOrEmpty(info.Ref, nameof(info.Ref)); + return $"{info.NameWithOwner}@{info.Ref}"; + } + + private AuthenticationHeaderValue CreateAuthHeader(string token) + { + if (string.IsNullOrEmpty(token)) { 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()); + var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}")); + HostContext.SecretMasker.AddValue(base64EncodingToken); + return new AuthenticationHeaderValue("Basic", base64EncodingToken); } // todo: Remove when feature flag DistributedTask.NewActionMetadata is removed @@ -1166,17 +1184,6 @@ 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/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 7bfe0932a..0318974b0 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -44,6 +44,7 @@ namespace GitHub.Runner.Worker TaskResult? CommandResult { get; set; } CancellationToken CancellationToken { get; } List Endpoints { get; } + TaskOrchestrationPlanReference Plan { get; } PlanFeatures Features { get; } Variables Variables { get; } @@ -141,6 +142,7 @@ namespace GitHub.Runner.Worker public Task ForceCompleted => _forceCompleted.Task; public CancellationToken CancellationToken => _cancellationTokenSource.Token; public List Endpoints { get; private set; } + public TaskOrchestrationPlanReference Plan { get; private set; } public Variables Variables { get; private set; } public Dictionary IntraActionState { get; private set; } public IDictionary> JobDefaults { get; private set; } @@ -275,6 +277,7 @@ namespace GitHub.Runner.Worker child.Features = Features; child.Variables = Variables; child.Endpoints = Endpoints; + child.Plan = Plan; if (intraActionState == null) { child.IntraActionState = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -576,7 +579,8 @@ namespace GitHub.Runner.Worker _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); - // Features + // Plan + Plan = message.Plan; Features = PlanUtil.GetFeatures(message.Plan); // Endpoints diff --git a/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs b/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs index 867b4f927..91adde925 100644 --- a/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs +++ b/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs @@ -317,5 +317,37 @@ namespace GitHub.DistributedTask.WebApi userState: userState, cancellationToken: cancellationToken); } + + /// + /// [Preview API] Resolves information required to download actions (URL, token) defined in an orchestration. + /// + /// The project GUID to scope the request + /// The name of the server hub: "build" for the Build server or "rm" for the Release Management server + /// + /// + /// + /// The cancellation token to cancel operation. + public virtual Task ResolveActionDownloadInfoAsync( + Guid scopeIdentifier, + string hubName, + Guid planId, + ActionReferenceList actionReferenceList, + object userState = null, + CancellationToken cancellationToken = default) + { + HttpMethod httpMethod = new HttpMethod("POST"); + Guid locationId = new Guid("27d7f831-88c1-4719-8ca1-6a061dad90eb"); + object routeValues = new { scopeIdentifier = scopeIdentifier, hubName = hubName, planId = planId }; + HttpContent content = new ObjectContent(actionReferenceList, new VssJsonMediaTypeFormatter(true)); + + return SendAsync( + httpMethod, + locationId, + routeValues: routeValues, + version: new ApiResourceVersion(6.0, 1), + userState: userState, + cancellationToken: cancellationToken, + content: content); + } } } diff --git a/src/Sdk/DTWebApi/WebApi/ActionDownloadInfo.cs b/src/Sdk/DTWebApi/WebApi/ActionDownloadInfo.cs new file mode 100644 index 000000000..a6b0749f6 --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/ActionDownloadInfo.cs @@ -0,0 +1,40 @@ +using System; +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public class ActionDownloadInfo + { + [DataMember(EmitDefaultValue = false)] + public ActionDownloadAuthentication Authentication { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string NameWithOwner { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string ResolvedNameWithOwner { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string ResolvedSha { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string TarballUrl { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string Ref { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string ZipballUrl { get; set; } + } + + [DataContract] + public class ActionDownloadAuthentication + { + [DataMember(EmitDefaultValue = false)] + public DateTime ExpiresAt { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string Token { get; set; } + } +} diff --git a/src/Sdk/DTWebApi/WebApi/ActionDownloadInfoCollection.cs b/src/Sdk/DTWebApi/WebApi/ActionDownloadInfoCollection.cs new file mode 100644 index 000000000..1367bf860 --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/ActionDownloadInfoCollection.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public class ActionDownloadInfoCollection + { + [DataMember] + public IDictionary Actions + { + get; + set; + } + } +} diff --git a/src/Sdk/DTWebApi/WebApi/ActionReference.cs b/src/Sdk/DTWebApi/WebApi/ActionReference.cs new file mode 100644 index 000000000..c6ea8a9ed --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/ActionReference.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public class ActionReference + { + [DataMember] + public string NameWithOwner + { + get; + set; + } + + [DataMember] + public string Ref + { + get; + set; + } + } +} diff --git a/src/Sdk/DTWebApi/WebApi/ActionReferenceList.cs b/src/Sdk/DTWebApi/WebApi/ActionReferenceList.cs new file mode 100644 index 000000000..b118b9904 --- /dev/null +++ b/src/Sdk/DTWebApi/WebApi/ActionReferenceList.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.WebApi +{ + [DataContract] + public class ActionReferenceList + { + [DataMember] + public IList Actions + { + get; + set; + } + } +} diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index fb20b2356..85851a55a 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -28,6 +28,7 @@ namespace GitHub.Runner.Common.Tests.Worker private Mock _configurationStore; private Mock _dockerManager; private Mock _ec; + private Mock _jobServer; private Mock _pluginManager; private TestHostContext _hc; private ActionManager _actionManager; @@ -3583,6 +3584,7 @@ runs: _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.Plan).Returns(new TaskOrchestrationPlanReference()); _ec.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); }); _ec.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); }); _ec.Setup(x => x.GetGitHubContext("workspace")).Returns(Path.Combine(_workFolder, "actions", "actions")); @@ -3593,6 +3595,25 @@ runs: _dockerManager.Setup(x => x.DockerBuild(_ec.Object, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(0)); + _jobServer = new Mock(); + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + TarballUrl = $"/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + _pluginManager = new Mock(); _pluginManager.Setup(x => x.GetPluginAction(It.IsAny())).Returns(new RunnerPluginActionInfo() { PluginTypeName = "plugin.class, plugin", PostPluginTypeName = "plugin.cleanup, plugin" }); @@ -3600,6 +3621,7 @@ runs: actionManifest.Initialize(_hc); _hc.SetSingleton(_dockerManager.Object); + _hc.SetSingleton(_jobServer.Object); _hc.SetSingleton(_pluginManager.Object); _hc.SetSingleton(actionManifest); _hc.SetSingleton(new HttpClientHandlerFactory());