tgstation-server 6.16.0
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(
83 repositorySettings.AccessToken!,
84 new RepositoryIdentifier(remoteInformation),
85 cancellationToken);
86
87 if (authenticatedGitHubService == null)
88 {
89 Logger.LogWarning("Can't create GitHub deployment as authentication for repository failed!");
90 gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
91 }
92 else
93 gitHubService = authenticatedGitHubService;
94 }
95 else
96 {
97 authenticatedGitHubService = null;
98 gitHubService = await gitHubServiceFactory.CreateService(cancellationToken);
99 }
100
101 var repoOwner = remoteInformation.RemoteRepositoryOwner!;
102 var repoName = remoteInformation.RemoteRepositoryName!;
103 var repositoryIdTask = gitHubService.GetRepositoryId(
104 repoOwner,
105 repoName,
106 cancellationToken);
107
108 if (!repositorySettings.CreateGitHubDeployments!.Value)
109 Logger.LogTrace("Not creating deployment");
110 else if (!instanceAuthenticated)
111 Logger.LogWarning("Can't create GitHub deployment as no access token is set for repository!");
112 else if (authenticatedGitHubService != null)
113 {
114 Logger.LogTrace("Creating deployment...");
115 try
116 {
117 compileJob.GitHubDeploymentId = await authenticatedGitHubService.CreateDeployment(
118 new NewDeployment(compileJob.RevisionInformation.CommitSha)
119 {
120 AutoMerge = false,
121 Description = "TGS Game Deployment",
122 Environment = $"TGS: {Metadata.Name}",
123 ProductionEnvironment = true,
124 RequiredContexts = new Collection<string>(),
125 },
126 repoOwner,
127 repoName,
128 cancellationToken);
129
130 Logger.LogDebug("Created deployment ID {deploymentId}", compileJob.GitHubDeploymentId);
131
132 await authenticatedGitHubService.CreateDeploymentStatus(
133 new NewDeploymentStatus(DeploymentState.InProgress)
134 {
135 Description = "The project is being deployed",
136 AutoInactive = false,
137 },
138 repoOwner,
139 repoName,
140 compileJob.GitHubDeploymentId.Value,
141 cancellationToken);
142
143 Logger.LogTrace("In-progress deployment status created");
144 }
145 catch (Exception ex) when (ex is not OperationCanceledException)
146 {
147 Logger.LogWarning(ex, "Unable to create GitHub deployment!");
148 }
149 }
150
151 try
152 {
153 compileJob.GitHubRepoId = await repositoryIdTask;
154 Logger.LogTrace("Set GitHub ID as {gitHubRepoId}", compileJob.GitHubRepoId);
155 }
156 catch (Exception ex) when (ex is not OperationCanceledException)
157 {
158 Logger.LogWarning(ex, "Unable to set compile job repository ID!");
159 }
160 }
161
163 public override ValueTask FailDeployment(CompileJob compileJob, string errorMessage, CancellationToken cancellationToken)
165 compileJob,
166 errorMessage,
167 DeploymentState.Error,
168 cancellationToken);
169
171 public override async ValueTask<IReadOnlyCollection<TestMerge>> RemoveMergedTestMerges(
172 IRepository repository,
173 RepositorySettings repositorySettings,
174 RevisionInformation revisionInformation,
175 CancellationToken cancellationToken)
176 {
177 ArgumentNullException.ThrowIfNull(repository);
178 ArgumentNullException.ThrowIfNull(repositorySettings);
179 ArgumentNullException.ThrowIfNull(revisionInformation);
180
181 if ((revisionInformation.ActiveTestMerges?.Count > 0) != true)
182 {
183 Logger.LogTrace("No test merges to remove.");
184 return Array.Empty<TestMerge>();
185 }
186
187 var gitHubService = repositorySettings.AccessToken != null
189 repositorySettings.AccessToken,
190 new RepositoryIdentifier(repository),
191 cancellationToken)
192 ?? await gitHubServiceFactory.CreateService(cancellationToken)
193 : await gitHubServiceFactory.CreateService(cancellationToken);
194
195 var tasks = revisionInformation
197 .Select(x => gitHubService.GetPullRequest(
198 repository.RemoteRepositoryOwner!,
199 repository.RemoteRepositoryName!,
200 x.TestMerge.Number,
201 cancellationToken));
202 try
203 {
204 await Task.WhenAll(tasks);
205 }
206 catch (Exception ex) when (ex is not OperationCanceledException)
207 {
208 Logger.LogWarning(ex, "Pull requests update check failed!");
209 }
210
211 var newList = revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList();
212
213 PullRequest? lastMerged = null;
214 async ValueTask CheckRemovePR(Task<PullRequest> task)
215 {
216 var pr = await task;
217 if (!pr.Merged)
218 return;
219
220 // We don't just assume, actually check the repo contains the merge commit.
221 if (await repository.CommittishIsParent(pr.MergeCommitSha, cancellationToken))
222 {
223 if (lastMerged == null || lastMerged.MergedAt < pr.MergedAt)
224 lastMerged = pr;
225 newList.Remove(
226 newList.First(
227 potential => potential.Number == pr.Number));
228 }
229 }
230
231 foreach (var prTask in tasks)
232 await CheckRemovePR(prTask);
233
234 return newList;
235 }
236
238 protected override ValueTask StageDeploymentImpl(
239 CompileJob compileJob,
240 CancellationToken cancellationToken)
242 compileJob,
243 "The deployment succeeded and will be applied a the next server reboot.",
244 DeploymentState.Pending,
245 cancellationToken);
246
248 protected override ValueTask ApplyDeploymentImpl(CompileJob compileJob, CancellationToken cancellationToken)
250 compileJob,
251 "The deployment is now live on the server.",
252 DeploymentState.Success,
253 cancellationToken);
254
256 protected override ValueTask MarkInactiveImpl(CompileJob compileJob, CancellationToken cancellationToken)
258 compileJob,
259 "The deployment has been superceeded.",
260 DeploymentState.Inactive,
261 cancellationToken);
262
264 protected override async ValueTask CommentOnTestMergeSource(
265 RepositorySettings repositorySettings,
266 string remoteRepositoryOwner,
267 string remoteRepositoryName,
268 string comment,
269 int testMergeNumber,
270 CancellationToken cancellationToken)
271 {
272 var gitHubService = await gitHubServiceFactory.CreateService(
273 repositorySettings.AccessToken!,
274 new RepositoryIdentifier(remoteRepositoryOwner, remoteRepositoryName),
275 cancellationToken);
276
277 if (gitHubService == null)
278 {
279 Logger.LogWarning("Error posting GitHub comment: Authentication failed!");
280 return;
281 }
282
283 try
284 {
285 string header = String.Format(CultureInfo.InvariantCulture, "{1}{0}## Test merge deployment history:{0}{0}", Environment.NewLine, DeploymentMsgHeaderStart);
286
287 var existingComment = await gitHubService.GetExistingCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, DeploymentMsgHeaderStart, testMergeNumber, cancellationToken);
288 if (existingComment != null)
289 {
290 await gitHubService.AppendCommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, existingComment, cancellationToken);
291 }
292 else
293 {
294 await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, header + comment, testMergeNumber, cancellationToken);
295 }
296 }
297 catch (Exception ex) when (ex is not OperationCanceledException)
298 {
299 Logger.LogWarning(ex, "Error posting GitHub comment!");
300 }
301 }
302
304 protected override string FormatTestMerge(
305 RepositorySettings repositorySettings,
306 CompileJob compileJob,
307 TestMerge testMerge,
308 string remoteRepositoryOwner,
309 string remoteRepositoryName,
310 bool updated) => String.Format(
311 CultureInfo.InvariantCulture,
312 "<details><summary>Test Merge {4} @ {8}:</summary>{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}{0}</details>{0}",
313 Environment.NewLine, // 0
314 repositorySettings.ShowTestMergeCommitters!.Value
315 ? String.Format(
316 CultureInfo.InvariantCulture,
317 "{0}{0}##### Merged By{0}{1}",
318 Environment.NewLine,
319 testMerge.MergedBy!.Name)
320 : String.Empty, // 1
321 testMerge.TargetCommitSha, // 2
322 String.IsNullOrEmpty(testMerge.Comment)
323 ? String.Empty
324 : String.Format(
325 CultureInfo.InvariantCulture,
326 "{0}{0}##### Comment{0}{1}",
327 Environment.NewLine,
328 testMerge.Comment), // 3
329 updated ? "Updated" : "Deployed", // 4
330 compileJob.GitHubDeploymentId.HasValue
331 ? $"{Environment.NewLine}[{Metadata.Name}](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
332 : Metadata.Name, // 5
333 compileJob.RevisionInformation.OriginCommitSha, // 6
334 compileJob.RevisionInformation.CommitSha, // 7
335 compileJob.Job.StartedAt); // 8
336
338 protected override string FormatTestMergeRemoval(
339 RepositorySettings repositorySettings,
340 CompileJob compileJob,
341 TestMerge testMerge,
342 string remoteRepositoryOwner,
343 string remoteRepositoryName) => String.Format(
344 CultureInfo.InvariantCulture,
345 "<details><summary>Test Merge Removed @ {2}:</summary>{0}{0}##### Server Instance{0}{1}{0}</details>{0}",
346 Environment.NewLine, // 0
347 Metadata.Name, // 1
348 compileJob.Job.StartedAt); // 2
349
358 async ValueTask UpdateDeployment(
359 CompileJob compileJob,
360 string description,
361 DeploymentState deploymentState,
362 CancellationToken cancellationToken)
363 {
364 ArgumentNullException.ThrowIfNull(compileJob);
365
366 if (!compileJob.GitHubRepoId.HasValue || !compileJob.GitHubDeploymentId.HasValue)
367 {
368 Logger.LogTrace("Not updating deployment as it is missing a repo ID or deployment ID.");
369 return;
370 }
371
372 Logger.LogTrace("Updating deployment {gitHubDeploymentId} to {deploymentState}...", compileJob.GitHubDeploymentId.Value, deploymentState);
373
374 string? gitHubAccessToken = null;
376 async databaseContext =>
377 gitHubAccessToken = await databaseContext
379 .AsQueryable()
380 .Where(x => x.InstanceId == Metadata.Id)
381 .Select(x => x.AccessToken)
382 .FirstAsync(cancellationToken));
383
384 if (gitHubAccessToken == null)
385 {
386 Logger.LogWarning(
387 "GitHub access token disappeared during deployment, can't update to {deploymentState}!",
388 deploymentState);
389 return;
390 }
391
392 var gitHubService = await gitHubServiceFactory.CreateService(
393 gitHubAccessToken,
394 new RepositoryIdentifier(compileJob.GitHubRepoId.Value),
395 cancellationToken);
396
397 if (gitHubService == null)
398 {
399 Logger.LogWarning(
400 "GitHub authentication failed, can't update to {deploymentState}!",
401 deploymentState);
402 return;
403 }
404
405 try
406 {
407 await gitHubService.CreateDeploymentStatus(
408 new NewDeploymentStatus(deploymentState)
409 {
410 Description = description,
411 },
412 compileJob.GitHubRepoId.Value,
413 compileJob.GitHubDeploymentId.Value,
414 cancellationToken);
415 }
416 catch (Exception ex) when (ex is not OperationCanceledException)
417 {
418 Logger.LogWarning(ex, "Error updating GitHub deployment!");
419 }
420 }
421 }
422}
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. Can also be a TGS encoded app private key....
Api.Models.Instance Metadata
The Api.Models.Instance for the BaseRemoteDeploymentManager.
const string DeploymentMsgHeaderStart
The header comment that begins every deployment message comment/note.
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...
override string FormatTestMergeRemoval(RepositorySettings repositorySettings, CompileJob compileJob, TestMerge testMerge, string remoteRepositoryOwner, string remoteRepositoryName)
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.
Identifies a repository either by its RepositoryId or Owner and Name.
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.