tgstation-server 6.9.2
The /tg/station 13 server suite
Loading...
Searching...
No Matches
GitHubRemoteDeploymentManager.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Collections.ObjectModel;
5using System.Globalization;
6using System.Linq;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.EntityFrameworkCore;
11using Microsoft.Extensions.Logging;
12
13using Octokit;
14
19
21{
26 {
31
36
48 ILogger<GitHubRemoteDeploymentManager> logger,
49 Api.Models.Instance metadata,
50 ConcurrentDictionary<long, Action<bool>> activationCallbacks)
51 : base(logger, metadata, activationCallbacks)
52 {
53 this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
54 this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory));
55 }
56
58 public override async ValueTask StartDeployment(
59 Api.Models.Internal.IGitRemoteInformation remoteInformation,
60 CompileJob compileJob,
61 CancellationToken cancellationToken)
62 {
63 ArgumentNullException.ThrowIfNull(remoteInformation);
64 ArgumentNullException.ThrowIfNull(compileJob);
65
66 Logger.LogTrace("Starting deployment...");
67
68 RepositorySettings? repositorySettings = null;
70 async databaseContext =>
71 repositorySettings = await databaseContext
73 .AsQueryable()
74 .Where(x => x.InstanceId == Metadata.Id)
75 .FirstAsync(cancellationToken));
76
77 var instanceAuthenticated = repositorySettings!.AccessToken != null;
78 IAuthenticatedGitHubService? authenticatedGitHubService;
79 IGitHubService gitHubService;
80 if (instanceAuthenticated)
81 {
82 authenticatedGitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
83 gitHubService = authenticatedGitHubService;
84 }
85 else
86 {
87 authenticatedGitHubService = null;
88 gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
89 }
90
91 var repoOwner = remoteInformation.RemoteRepositoryOwner!;
92 var repoName = remoteInformation.RemoteRepositoryName!;
93 var repositoryIdTask = gitHubService.GetRepositoryId(
94 repoOwner,
95 repoName,
96 cancellationToken);
97
98 if (!repositorySettings.CreateGitHubDeployments!.Value)
99 Logger.LogTrace("Not creating deployment");
100 else if (!instanceAuthenticated)
101 Logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!");
102 else
103 {
104 Logger.LogTrace("Creating deployment...");
105 try
106 {
107 compileJob.GitHubDeploymentId = await authenticatedGitHubService!.CreateDeployment(
108 new NewDeployment(compileJob.RevisionInformation.CommitSha)
109 {
110 AutoMerge = false,
111 Description = "TGS Game Deployment",
112 Environment = $"TGS: {Metadata.Name}",
113 ProductionEnvironment = true,
114 RequiredContexts = new Collection<string>(),
115 },
116 repoOwner,
117 repoName,
118 cancellationToken);
119
120 Logger.LogDebug("Created deployment ID {deploymentId}", compileJob.GitHubDeploymentId);
121
122 await authenticatedGitHubService.CreateDeploymentStatus(
123 new NewDeploymentStatus(DeploymentState.InProgress)
124 {
125 Description = "The project is being deployed",
126 AutoInactive = false,
127 },
128 repoOwner,
129 repoName,
130 compileJob.GitHubDeploymentId.Value,
131 cancellationToken);
132
133 Logger.LogTrace("In-progress deployment status created");
134 }
135 catch (Exception ex) when (ex is not OperationCanceledException)
136 {
137 Logger.LogWarning(ex, "Unable to create GitHub deployment!");
138 }
139 }
140
141 try
142 {
143 compileJob.GitHubRepoId = await repositoryIdTask;
144 Logger.LogTrace("Set GitHub ID as {gitHubRepoId}", compileJob.GitHubRepoId);
145 }
146 catch (Exception ex) when (ex is not OperationCanceledException)
147 {
148 Logger.LogWarning(ex, "Unable to set compile job repository ID!");
149 }
150 }
151
153 public override ValueTask FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken)
155 compileJob,
156 errorMessage,
157 DeploymentState.Error,
158 cancellationToken);
159
161 public override async ValueTask<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
162 IRepository repository,
163 RepositorySettings repositorySettings,
164 RevisionInformation revisionInformation,
165 CancellationToken cancellationToken)
166 {
167 ArgumentNullException.ThrowIfNull(repository);
168 ArgumentNullException.ThrowIfNull(repositorySettings);
169 ArgumentNullException.ThrowIfNull(revisionInformation);
170
171 if ((revisionInformation.ActiveTestMerges?.Count > 0) != true)
172 {
173 Logger.LogTrace("No test merges to remove.");
174 return Array.Empty<TestMerge>();
175 }
176
177 var gitHubService = repositorySettings.AccessToken != null
178 ? await gitHubServiceFactory.CreateService(repositorySettings.AccessToken, cancellationToken)
179 : await gitHubServiceFactory.CreateService(cancellationToken);
180
181 var tasks = revisionInformation
183 .Select(x => gitHubService.GetPullRequest(
184 repository.RemoteRepositoryOwner!,
185 repository.RemoteRepositoryName!,
186 x.TestMerge.Number,
187 cancellationToken));
188 try
189 {
190 await Task.WhenAll(tasks);
191 }
192 catch (Exception ex) when (ex is not OperationCanceledException)
193 {
194 Logger.LogWarning(ex, "Pull requests update check failed!");
195 }
196
197 var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
198
199 PullRequest? lastMerged = null;
200 async ValueTask CheckRemovePR(Task<PullRequest> task)
201 {
202 var pr = await task;
203 if (!pr.Merged)
204 return;
205
206 // We don't just assume, actually check the repo contains the merge commit.
207 if (await repository.CommittishIsParent(pr.MergeCommitSha, cancellationToken))
208 {
209 if (lastMerged == null || lastMerged.MergedAt < pr.MergedAt)
210 lastMerged = pr;
211 newList.Remove(
212 newList.First(
213 potential => potential.Number == pr.Number));
214 }
215 }
216
217 foreach (var prTask in tasks)
218 await CheckRemovePR(prTask);
219
220 return newList;
221 }
222
224 protected override ValueTask StageDeploymentImpl(
225 CompileJob compileJob,
226 CancellationToken cancellationToken)
228 compileJob,
229 "The deployment succeeded and will be applied a the next server reboot.",
230 DeploymentState.Pending,
231 cancellationToken);
232
234 protected override ValueTask ApplyDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken)
236 compileJob,
237 "The deployment is now live on the server.",
238 DeploymentState.Success,
239 cancellationToken);
240
242 protected override ValueTask MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken)
244 compileJob,
245 "The deployment has been superceeded.",
246 DeploymentState.Inactive,
247 cancellationToken);
248
250 protected override async ValueTask CommentOnTestMergeSource(
251 RepositorySettings repositorySettings,
252 string remoteRepositoryOwner,
253 string remoteRepositoryName,
254 string comment,
255 int testMergeNumber,
256 CancellationToken cancellationToken)
257 {
258 var gitHubService = await gitHubServiceFactory.CreateService(repositorySettings.AccessToken!, cancellationToken);
259
260 try
261 {
262 await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken);
263 }
264 catch (Exception ex) when (ex is not OperationCanceledException)
265 {
266 Logger.LogWarning(ex, "Error posting GitHub comment!");
267 }
268 }
269
271 protected override string FormatTestMerge(
272 RepositorySettings repositorySettings,
273 CompileJob compileJob,
274 TestMerge testMerge,
275 string remoteRepositoryOwner,
276 string remoteRepositoryName,
277 bool updated) => String.Format(
278 CultureInfo.InvariantCulture,
279 "#### Test Merge {4}{0}{0}<details><summary>Details</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{8}{0}</details>",
280 Environment.NewLine,
281 repositorySettings.ShowTestMergeCommitters!.Value
282 ? String.Format(
283 CultureInfo.InvariantCulture,
284 "{0}{0}##### Merged By{0}{1}",
285 Environment.NewLine,
286 testMerge.MergedBy!.Name)
287 : String.Empty,
288 testMerge.TargetCommitSha,
289 testMerge.Comment != null
290 ? String.Format(
291 CultureInfo.InvariantCulture,
292 "{0}{0}##### Comment{0}{1}",
293 Environment.NewLine,
294 testMerge.Comment)
295 : String.Empty,
296 updated ? "Updated" : "Deployed",
297 Metadata.Name,
298 compileJob.RevisionInformation.OriginCommitSha,
299 compileJob.RevisionInformation.CommitSha,
300 compileJob.GitHubDeploymentId.HasValue
301 ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
302 : String.Empty);
303
312 async ValueTask UpdateDeployment(
313 CompileJob compileJob,
314 string description,
315 DeploymentState deploymentState,
316 CancellationToken cancellationToken)
317 {
318 ArgumentNullException.ThrowIfNull(compileJob);
319
320 if (!compileJob.GitHubRepoId.HasValue || !compileJob.GitHubDeploymentId.HasValue)
321 {
322 Logger.LogTrace("Not updating deployment as it is missing a repo ID or deployment ID.");
323 return;
324 }
325
326 Logger.LogTrace("Updating deployment {gitHubDeploymentId} to {deploymentState}...", compileJob.GitHubDeploymentId.Value, deploymentState);
327
328 string? gitHubAccessToken = null;
330 async databaseContext =>
331 gitHubAccessToken = await databaseContext
333 .AsQueryable()
334 .Where(x => x.InstanceId == Metadata.Id)
335 .Select(x => x.AccessToken)
336 .FirstAsync(cancellationToken));
337
338 if (gitHubAccessToken == null)
339 {
340 Logger.LogWarning(
341 "GitHub access token disappeared during deployment, can't update to {deploymentState}!",
342 deploymentState);
343 return;
344 }
345
346 var gitHubService = await gitHubServiceFactory.CreateService(gitHubAccessToken, cancellationToken);
347
348 try
349 {
350 await gitHubService.CreateDeploymentStatus(
351 new NewDeploymentStatus(deploymentState)
352 {
353 Description = description,
354 },
355 compileJob.GitHubRepoId.Value,
356 compileJob.GitHubDeploymentId.Value,
357 cancellationToken);
358 }
359 catch (Exception ex) when (ex is not OperationCanceledException)
360 {
361 Logger.LogWarning(ex, "Error updating GitHub deployment!");
362 }
363 }
364 }
365}
bool? CreateGitHubDeployments
If GitHub deployments should be created. Requires AccessUser, AccessToken, and PushTestMergeCommits t...
string? AccessToken
The token/password to access the git repository with.
Api.Models.Instance Metadata
The Api.Models.Instance for the BaseRemoteDeploymentManager.
readonly ConcurrentDictionary< long, Action< bool > > activationCallbacks
A map of CompileJob Api.Models.EntityId.Ids to activation callback Action<T1>s.
ILogger< BaseRemoteDeploymentManager > Logger
The ILogger for the BaseRemoteDeploymentManager.
override async ValueTask CommentOnTestMergeSource(RepositorySettings repositorySettings, string remoteRepositoryOwner, string remoteRepositoryName, string comment, int testMergeNumber, CancellationToken cancellationToken)
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the GitHubRemoteDeploymentManager.
readonly IGitHubServiceFactory gitHubServiceFactory
The IGitHubServiceFactory for the GitHubRemoteDeploymentManager.
async ValueTask UpdateDeployment(CompileJob compileJob, string description, DeploymentState deploymentState, CancellationToken cancellationToken)
Update the deployment for a given compileJob .
override async ValueTask< IReadOnlyCollection< TestMerge > > RemoveMergedTestMerges(IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken)
Get the updated list of TestMerges for an origin merge.A ValueTask<TResult> resulting in the IReadOnl...
GitHubRemoteDeploymentManager(IDatabaseContextFactory databaseContextFactory, IGitHubServiceFactory gitHubServiceFactory, ILogger< GitHubRemoteDeploymentManager > logger, Api.Models.Instance metadata, ConcurrentDictionary< long, Action< bool > > activationCallbacks)
Initializes a new instance of the GitHubRemoteDeploymentManager class.
override ValueTask MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken)
override ValueTask FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken)
Fail a deployment for a given compileJob .A ValueTask representing the running operation.
override async ValueTask StartDeployment(Api.Models.Internal.IGitRemoteInformation remoteInformation, CompileJob compileJob, CancellationToken cancellationToken)
Start a deployment for a given compileJob .A ValueTask representing the running operation.
override string FormatTestMerge(RepositorySettings repositorySettings, CompileJob compileJob, TestMerge testMerge, string remoteRepositoryOwner, string remoteRepositoryName, bool updated)
override ValueTask StageDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken)
override ValueTask ApplyDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken)
RevisionInformation RevisionInformation
See CompileJobResponse.RevisionInformation.
Definition CompileJob.cs:27
long? GitHubDeploymentId
The GitHub deployment ID associated with the CompileJob if any.
Definition CompileJob.cs:63
long? GitHubRepoId
The source GitHub repository the deployment came from if any.
Definition CompileJob.cs:58
ICollection< RevInfoTestMerge >? ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges.
string? RemoteRepositoryName
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the name of the repositor...
string? RemoteRepositoryOwner
If RemoteGitProvider is not RemoteGitProvider.Unknown this will be set with the owner of the reposito...
Represents an on-disk git repository.
Task< bool > CommittishIsParent(string committish, CancellationToken cancellationToken)
Check if a given committish is a parent of the current Head.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
ValueTask UseContext(Func< IDatabaseContext, ValueTask > operation)
Run an operation in the scope of an IDatabaseContext.
IGitHubService that exposes functions that require authentication.
ValueTask< long > CreateDeployment(NewDeployment newDeployment, string repoOwner, string repoName, CancellationToken cancellationToken)
Create a newDeployment on a target repostiory.
Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, string repoOwner, string repoName, long deploymentId, CancellationToken cancellationToken)
Create a newDeploymentStatus on a target deployment.
ValueTask< IGitHubService > CreateService(CancellationToken cancellationToken)
Create a IGitHubService.
Service for interacting with the GitHub API.
ValueTask< long > GetRepositoryId(string repoOwner, string repoName, CancellationToken cancellationToken)
Get a target repostiory's ID.