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