tgstation-server 6.12.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 await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken);
286 }
287 catch (Exception ex) when (ex is not OperationCanceledException)
288 {
289 Logger.LogWarning(ex, "Error posting GitHub comment!");
290 }
291 }
292
294 protected override string FormatTestMerge(
295 RepositorySettings repositorySettings,
296 CompileJob compileJob,
297 TestMerge testMerge,
298 string remoteRepositoryOwner,
299 string remoteRepositoryName,
300 bool updated) => String.Format(
301 CultureInfo.InvariantCulture,
302 "#### 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>",
303 Environment.NewLine,
304 repositorySettings.ShowTestMergeCommitters!.Value
305 ? String.Format(
306 CultureInfo.InvariantCulture,
307 "{0}{0}##### Merged By{0}{1}",
308 Environment.NewLine,
309 testMerge.MergedBy!.Name)
310 : String.Empty,
311 testMerge.TargetCommitSha,
312 testMerge.Comment != null
313 ? String.Format(
314 CultureInfo.InvariantCulture,
315 "{0}{0}##### Comment{0}{1}",
316 Environment.NewLine,
317 testMerge.Comment)
318 : String.Empty,
319 updated ? "Updated" : "Deployed",
320 Metadata.Name,
321 compileJob.RevisionInformation.OriginCommitSha,
322 compileJob.RevisionInformation.CommitSha,
323 compileJob.GitHubDeploymentId.HasValue
324 ? $"{Environment.NewLine}[GitHub Deployments](https://github.com/{remoteRepositoryOwner}/{remoteRepositoryName}/deployments/activity_log?environment=TGS%3A+{Metadata.Name!.Replace(" ", "+", StringComparison.Ordinal)})"
325 : String.Empty);
326
335 async ValueTask UpdateDeployment(
336 CompileJob compileJob,
337 string description,
338 DeploymentState deploymentState,
339 CancellationToken cancellationToken)
340 {
341 ArgumentNullException.ThrowIfNull(compileJob);
342
343 if (!compileJob.GitHubRepoId.HasValue || !compileJob.GitHubDeploymentId.HasValue)
344 {
345 Logger.LogTrace("Not updating deployment as it is missing a repo ID or deployment ID.");
346 return;
347 }
348
349 Logger.LogTrace("Updating deployment {gitHubDeploymentId} to {deploymentState}...", compileJob.GitHubDeploymentId.Value, deploymentState);
350
351 string? gitHubAccessToken = null;
353 async databaseContext =>
354 gitHubAccessToken = await databaseContext
356 .AsQueryable()
357 .Where(x => x.InstanceId == Metadata.Id)
358 .Select(x => x.AccessToken)
359 .FirstAsync(cancellationToken));
360
361 if (gitHubAccessToken == null)
362 {
363 Logger.LogWarning(
364 "GitHub access token disappeared during deployment, can't update to {deploymentState}!",
365 deploymentState);
366 return;
367 }
368
369 var gitHubService = await gitHubServiceFactory.CreateService(
370 gitHubAccessToken,
371 new RepositoryIdentifier(compileJob.GitHubRepoId.Value),
372 cancellationToken);
373
374 if (gitHubService == null)
375 {
376 Logger.LogWarning(
377 "GitHub authentication failed, can't update to {deploymentState}!",
378 deploymentState);
379 return;
380 }
381
382 try
383 {
384 await gitHubService.CreateDeploymentStatus(
385 new NewDeploymentStatus(deploymentState)
386 {
387 Description = description,
388 },
389 compileJob.GitHubRepoId.Value,
390 compileJob.GitHubDeploymentId.Value,
391 cancellationToken);
392 }
393 catch (Exception ex) when (ex is not OperationCanceledException)
394 {
395 Logger.LogWarning(ex, "Error updating GitHub deployment!");
396 }
397 }
398 }
399}
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.
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.
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.