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