diff --git a/src/Runner.Common/ExtensionManager.cs b/src/Runner.Common/ExtensionManager.cs index c53fc3647..eae67de68 100644 --- a/src/Runner.Common/ExtensionManager.cs +++ b/src/Runner.Common/ExtensionManager.cs @@ -46,6 +46,7 @@ namespace GitHub.Runner.Common Add(extensions, "GitHub.Runner.Worker.SetOutputCommandExtension, Runner.Worker"); Add(extensions, "GitHub.Runner.Worker.SaveStateCommandExtension, Runner.Worker"); Add(extensions, "GitHub.Runner.Worker.AddPathCommandExtension, Runner.Worker"); + Add(extensions, "GitHub.Runner.Worker.RefreshTokenCommandExtension, Runner.Worker"); Add(extensions, "GitHub.Runner.Worker.AddMaskCommandExtension, Runner.Worker"); Add(extensions, "GitHub.Runner.Worker.AddMatcherCommandExtension, Runner.Worker"); Add(extensions, "GitHub.Runner.Worker.RemoveMatcherCommandExtension, Runner.Worker"); diff --git a/src/Runner.Common/JobServer.cs b/src/Runner.Common/JobServer.cs index 860555419..712e51d3a 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 RefreshGitHubTokenAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, CancellationToken cancellationToken); } public sealed class JobServer : RunnerService, IJobServer @@ -113,5 +114,11 @@ namespace GitHub.Runner.Common CheckConnection(); return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken); } + + public Task RefreshGitHubTokenAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, CancellationToken cancellationToken) + { + CheckConnection(); + return _taskClient.RefreshTokenAsync(scopeIdentifier, hubName, planId, jobId, cancellationToken: cancellationToken); + } } } diff --git a/src/Runner.Worker/ActionCommandManager.cs b/src/Runner.Worker/ActionCommandManager.cs index 29bd4a03b..8f3fa6979 100644 --- a/src/Runner.Worker/ActionCommandManager.cs +++ b/src/Runner.Worker/ActionCommandManager.cs @@ -288,6 +288,30 @@ namespace GitHub.Runner.Worker } } + public sealed class RefreshTokenCommandExtension : RunnerService, IActionCommandExtension + { + public string Command => "refresh-token"; + public bool OmitEcho => false; + + public Type ExtensionType => typeof(IActionCommandExtension); + + public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, ContainerInfo container) + { + ArgUtil.NotNullOrEmpty(command.Data, "token file"); + var runnerTemp = HostContext.GetDirectory(WellKnownDirectory.Temp); + var tempFile = Path.Combine(runnerTemp, command.Data); + if (!tempFile.StartsWith(runnerTemp + Path.DirectorySeparatorChar)) + { + throw new Exception($"'{command.Data}' has to be under runner temp directory '{runnerTemp}'"); + } + + Trace.Info("Here"); + var githubToken = context.GetGitHubToken().GetAwaiter().GetResult(); + File.WriteAllText(tempFile, githubToken); + Trace.Info("HereAgain"); + } + } + public sealed class AddMatcherCommandExtension : RunnerService, IActionCommandExtension { public string Command => "add-matcher"; diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 75f8562b0..497085354 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -21,6 +21,7 @@ using System.Collections; using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating; using Pipelines = GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.Expressions2; +using System.Diagnostics; namespace GitHub.Runner.Worker { @@ -99,6 +100,8 @@ namespace GitHub.Runner.Worker // others void ForceTaskComplete(); void RegisterPostJobStep(IStep step); + Task GetGitHubToken(); + Task UpdateGitHubTokenInContext(); } public sealed class ExecutionContext : RunnerService, IExecutionContext @@ -110,6 +113,7 @@ namespace GitHub.Runner.Worker private readonly object _loggerLock = new object(); private readonly HashSet _outputvariables = new HashSet(StringComparer.OrdinalIgnoreCase); private readonly object _matchersLock = new object(); + public readonly List _getGitHubTokenTasks = new List(); private event OnMatcherChanged _onMatcherChanged; @@ -130,6 +134,13 @@ namespace GitHub.Runner.Worker // only job level ExecutionContext will track throttling delay. private long _totalThrottlingDelayInMilliseconds = 0; + private Guid _jobId; + private Guid _scopeIdentifier; + private string _hubName; + private Guid _planId; + + private Stopwatch _githubTokenExpireTimer = new Stopwatch(); + public Guid Id => _record.Id; public string ScopeName { get; private set; } public string ContextName { get; private set; } @@ -242,6 +253,59 @@ namespace GitHub.Runner.Worker }); } + public Task GetGitHubToken() + { + Trace.Info($"Try get new GITHUB_TOKEN"); + return Root.RefreshGitHubToken(); + } + + public async Task UpdateGitHubTokenInContext() + { + try + { + await Root.EnsureGitHubToken(); + } + catch (Exception ex) + { + this.Warning($"Fail to get a new GITHUB_TOKEN, error: {ex.Message}"); + this.Debug(ex.ToString()); + } + } + + private async Task RefreshGitHubToken() + { + Trace.Info("Request new GITHUB_TOKEN"); + var jobServer = HostContext.GetService(); + var githubToken = await jobServer.RefreshGitHubTokenAsync(_scopeIdentifier, _hubName, _planId, _jobId, CancellationToken.None); + + if (!string.IsNullOrEmpty(githubToken?.Token)) + { + // register secret masker + Trace.Info("Register secret masker for new GITHUB_TOKEN"); + HostContext.SecretMasker.AddValue(githubToken.Token); + return githubToken.Token; + } + else + { + throw new InvalidOperationException("Get empty GTIHUB_TOKEN."); + } + } + + private async Task EnsureGitHubToken() + { + // needs to refresh GITHUB_TOKEN every 50 mins since the token is good for 60 min by default + if (_githubTokenExpireTimer.Elapsed.TotalMilliseconds > 10) + { + var githubToken = await this.RefreshGitHubToken(); + var secretsContext = ExpressionValues["secrets"] as DictionaryContextData; + secretsContext["GITHUB_TOKEN"] = new StringContextData(githubToken); + var githubContext = ExpressionValues["github"] as GitHubContext; + githubContext["token"] = new StringContextData(githubToken); + + _githubTokenExpireTimer.Restart(); + } + } + public void RegisterPostJobStep(IStep step) { if (step is IActionRunner actionRunner && !Root.StepsWithPostRegisted.Add(actionRunner.Action.Id)) @@ -617,6 +681,7 @@ namespace GitHub.Runner.Worker ExpressionValues["env"] = new CaseSensitiveDictionaryContextData(); #endif + _githubTokenExpireTimer.Start(); // Prepend Path PrependPath = new List(); @@ -630,6 +695,11 @@ namespace GitHub.Runner.Worker // StepsWithPostRegisted for job ExecutionContext StepsWithPostRegisted = new HashSet(); + _scopeIdentifier = message.Plan.ScopeIdentifier; + _hubName = message.Plan.PlanType; + _jobId = message.JobId; + _planId = message.Plan.PlanId; + // Job timeline record. InitializeTimelineRecord( timelineId: message.Timeline.Id, diff --git a/src/Runner.Worker/StepsRunner.cs b/src/Runner.Worker/StepsRunner.cs index 82cb8b9aa..2341a8e4f 100644 --- a/src/Runner.Worker/StepsRunner.cs +++ b/src/Runner.Worker/StepsRunner.cs @@ -75,6 +75,7 @@ namespace GitHub.Runner.Worker // Start step.ExecutionContext.Start(); + await step.ExecutionContext.UpdateGitHubTokenInContext(); // Initialize scope if (InitializeScope(step, scopeInputs)) diff --git a/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs b/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs index 867b4f927..f7119621c 100644 --- a/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs +++ b/src/Sdk/DTGenerated/Generated/TaskHttpClientBase.cs @@ -317,5 +317,40 @@ namespace GitHub.DistributedTask.WebApi userState: userState, cancellationToken: cancellationToken); } + + + /// + /// [Preview API] + /// + /// 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 RefreshTokenAsync( + Guid scopeIdentifier, + string hubName, + Guid planId, + Guid jobId, + object userState = null, + CancellationToken cancellationToken = default) + { + HttpMethod httpMethod = new HttpMethod("GET"); + Guid locationId = new Guid("8aa8aff7-751b-496e-be8d-b7818770efb3"); + object routeValues = new { scopeIdentifier = scopeIdentifier, hubName = hubName, planId = planId }; + + List> queryParams = new List>(); + queryParams.Add("jobId", jobId.ToString()); + + return SendAsync( + httpMethod, + locationId, + routeValues: routeValues, + version: new ApiResourceVersion(6.0, 1), + queryParameters: queryParams, + userState: userState, + cancellationToken: cancellationToken); + } } } diff --git a/src/Sdk/DTWebApi/WebApi/Issue.cs b/src/Sdk/DTWebApi/WebApi/Issue.cs index 4875ca5a1..63d31b695 100644 --- a/src/Sdk/DTWebApi/WebApi/Issue.cs +++ b/src/Sdk/DTWebApi/WebApi/Issue.cs @@ -4,6 +4,17 @@ using System.Runtime.Serialization; namespace GitHub.DistributedTask.WebApi { + + [DataContract] + public class GitHubToken + { + [DataMember(EmitDefaultValue = false)] + public string Token { get; set; } + + [DataMember(EmitDefaultValue = false)] + public DateTime Expires_at { get; set; } + } + [DataContract] public class Issue {