public Result Store(RevisionInformation revisionInformation) { try { using (TransactionScope scope = new TransactionScope()) { SqlConnection conn = (SqlConnection)DatabaseManager.DatabaseEngine.GetConnection(); SqlCommand cmd = new SqlCommand("StoreRevisionInformation", conn); cmd.CommandType = CommandType.StoredProcedure; if (revisionInformation.RevisionID == 0) { revisionInformation.RevisionID = DatabaseManager.GetUniqueID(); } cmd.Parameters.Add(new SqlParameter("@RevisionID", revisionInformation.RevisionID)); cmd.Parameters.Add(NewParameter("@RevisionSourceID", revisionInformation.RevisionSourceID, DbType.Int64)); cmd.Parameters.Add(NewParameter("@RevisionDate", revisionInformation.RevisionDate, DbType.DateTime)); cmd.Parameters.Add(NewParameter("@UserID", revisionInformation.UserID, DbType.Int64)); cmd.Parameters.Add(NewParameter("@Notes", revisionInformation.Notes, DbType.String)); cmd.Parameters.Add(NewParameter("@Hidden", revisionInformation.Hidden, DbType.Boolean)); cmd.Parameters.Add(NewParameter("@Draft", revisionInformation.Draft, DbType.Boolean)); cmd.Parameters.Add(NewParameter("@Deleted", revisionInformation.Deleted, DbType.Boolean)); cmd.ExecuteNonQuery(); scope.Complete(); } } catch (Exception ex) { return(new Result(ex.Message)); } finally { DatabaseManager.DatabaseEngine.ReleaseConnection(); } return(new Result()); }
/// <inheritdoc /> public Task PostDeploymentComments( CompileJob compileJob, RevisionInformation previousRevisionInformation, RepositorySettings repositorySettings, string repoOwner, string repoName, CancellationToken cancellationToken) => Task.CompletedTask;
public RevisionInformation SelectRevisionInformation(long revisionID) { using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress)) { using (SqlConnection conn = new SqlConnection(DatabaseManager.DatabaseEngine.ConnectionString)) { conn.Open(); SqlCommand cmd = new SqlCommand("SelectRevisionInformation", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new SqlParameter("@RevisionID", revisionID)); using (SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection)) { RevisionInformation entity; if (!reader.Read()) { entity = null; } else { entity = new RevisionInformation(reader); } reader.Close(); return(entity); } } } }
/// <inheritdoc /> public abstract Task <Func <string, string, Task> > SendUpdateMessage( RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken);
/// <summary> /// Run the compile job and insert it into the database /// </summary> /// <param name="job">The running <see cref="Job"/></param> /// <param name="serviceProvider">The <see cref="IServiceProvider"/> for the operation</param> /// <param name="instanceModel">The <see cref="Models.Instance"/> for the operation</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken) { var instanceManager = serviceProvider.GetRequiredService <IInstanceManager>(); var databaseContext = serviceProvider.GetRequiredService <IDatabaseContext>(); var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings { StartupTimeout = x.StartupTimeout, SecurityLevel = x.SecurityLevel }).FirstAsync(cancellationToken); var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).FirstAsync(cancellationToken).ConfigureAwait(false); var ddSettings = await ddSettingsTask.ConfigureAwait(false); var instance = instanceManager.GetInstance(instanceModel); CompileJob compileJob; RevisionInformation revInfo; using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { job.ExceptionDetails = "Missing repository!"; return; } var repoSha = repo.Head; revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); if (revInfo == default) { revInfo = new RevisionInformation { CommitSha = repoSha, OriginCommitSha = repoSha, Instance = new Models.Instance { Id = Instance.Id } }; databaseContext.Instances.Attach(revInfo.Instance); } compileJob = await instance.DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); } compileJob.Job = job; databaseContext.CompileJobs.Add(compileJob); await databaseContext.Save(cancellationToken).ConfigureAwait(false); await instance.CompileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); }
/// <inheritdoc /> public override async Task <Func <string, string, Task> > SendUpdateMessage( RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken) { bool gitHub = gitHubOwner != null && gitHubRepo != null; localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha; var fields = new List <EmbedFieldBuilder> { new EmbedFieldBuilder { Name = "BYOND Version", Value = $"{byondVersion.Major}.{byondVersion.Minor}", IsInline = true }, new EmbedFieldBuilder { Name = "Local Commit", Value = localCommitPushed && gitHub ? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})" : revisionInformation.CommitSha.Substring(0, 7), IsInline = true }, new EmbedFieldBuilder { Name = "Branch Commit", Value = gitHub ? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})" : revisionInformation.OriginCommitSha.Substring(0, 7), IsInline = true } }; fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty <RevInfoTestMerge>()) .Select(x => x.TestMerge) .Select(x => new EmbedFieldBuilder { Name = $"#{x.Number}", Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.PullRequestRevision.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.PullRequestRevision}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}" }));
/// <inheritdoc /> public override async Task <IReadOnlyCollection <RevInfoTestMerge> > RemoveMergedTestMerges( IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken) { if (repository == null) { throw new ArgumentNullException(nameof(repository)); } if (repositorySettings == null) { throw new ArgumentNullException(nameof(repositorySettings)); } if (revisionInformation == null) { throw new ArgumentNullException(nameof(revisionInformation)); } if (revisionInformation.ActiveTestMerges?.Any() != true) { Logger.LogTrace("No test merges to remove."); return(Array.Empty <RevInfoTestMerge>()); } var client = repositorySettings.AccessToken != null ? new GitLabClient(GitLabRemoteFeatures.GitLabUrl, repositorySettings.AccessToken) : new GitLabClient(GitLabRemoteFeatures.GitLabUrl); var tasks = revisionInformation .ActiveTestMerges .Select(x => client .MergeRequests .GetAsync( $"{repository.RemoteRepositoryOwner}/{repository.RemoteRepositoryName}", x.TestMerge.Number) .WithToken(cancellationToken)); try { await Task.WhenAll(tasks).ConfigureAwait(false); } catch (Exception ex) when(!(ex is OperationCanceledException)) { Logger.LogWarning(ex, "Merge requests update check failed!"); } var newList = revisionInformation.ActiveTestMerges.ToList(); MergeRequest lastMerged = null; async Task CheckRemoveMR(Task <MergeRequest> task) { var mergeRequest = await task.ConfigureAwait(false); if (mergeRequest.State != MergeRequestState.Merged) { return; } // We don't just assume, actually check the repo contains the merge commit. if (await repository.ShaIsParent(mergeRequest.MergeCommitSha, cancellationToken).ConfigureAwait(false)) { if (lastMerged == null || lastMerged.ClosedAt < mergeRequest.ClosedAt) { lastMerged = mergeRequest; } newList.Remove( newList.First( potential => potential.TestMerge.Number == mergeRequest.Id)); } } foreach (var prTask in tasks) { await CheckRemoveMR(prTask).ConfigureAwait(false); } return(newList); }
public Result Store(RevisionInformation revisionInformation) { try { using (TransactionScope scope = new TransactionScope()) { SQLiteConnection conn = (SQLiteConnection)DatabaseManager.DatabaseEngine.GetConnection(); SQLiteCommand cmd = new SQLiteCommand(Procedures["Store RevisionInformation"], conn); if (revisionInformation.RevisionID == 0) revisionInformation.RevisionID = DatabaseManager.GetUniqueID(); cmd.Parameters.Add(new SQLiteParameter("@RevisionID", revisionInformation.RevisionID)); cmd.Parameters.Add(NewParameter("@RevisionSourceID", revisionInformation.RevisionSourceID, DbType.Int64)); cmd.Parameters.Add(NewParameter("@RevisionDate", revisionInformation.RevisionDate, DbType.DateTime)); cmd.Parameters.Add(NewParameter("@UserID", revisionInformation.UserID, DbType.Int64)); cmd.Parameters.Add(NewParameter("@Notes", revisionInformation.Notes, DbType.String)); cmd.Parameters.Add(NewParameter("@Hidden", revisionInformation.Hidden, DbType.Boolean)); cmd.Parameters.Add(NewParameter("@Draft", revisionInformation.Draft, DbType.Boolean)); cmd.Parameters.Add(NewParameter("@Deleted", revisionInformation.Deleted, DbType.Boolean)); cmd.ExecuteNonQuery(); scope.Complete(); } } catch (Exception ex) { return new Result(ex.Message); } finally { DatabaseManager.DatabaseEngine.ReleaseConnection(); } return new Result(); }
public RevisionInformation SelectRevisionInformation(long revisionID) { using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress)) { using (SQLiteConnection conn = new SQLiteConnection(DatabaseManager.DatabaseEngine.ConnectionString)) { conn.Open(); SQLiteCommand cmd = new SQLiteCommand(Procedures["Select RevisionInformation"], conn); cmd.Parameters.Add(new SQLiteParameter("@RevisionID", revisionID)); using (SQLiteDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection)) { RevisionInformation entity; if (!reader.Read()) entity = null; else entity = new RevisionInformation(reader); reader.Close(); return entity; } } } }
/// <summary> /// Construct a <see cref="TestMerge"/> /// </summary> /// <param name="testMerge">The <see cref="Models.TestMerge"/> to build from</param> /// <param name="revision">The value of <see cref="Revision"/></param> public TestMerge(Models.TestMerge testMerge, RevisionInformation revision) : base(testMerge) { TimeMerged = testMerge.MergedAt.Ticks.ToString(CultureInfo.InvariantCulture); Revision = revision ?? throw new ArgumentNullException(nameof(revision)); }
/// <inheritdoc /> public Task <IReadOnlyCollection <RevInfoTestMerge> > RemoveMergedTestMerges( IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken) => Task.FromResult <IReadOnlyCollection <RevInfoTestMerge> >(Array.Empty <RevInfoTestMerge>());
/// <inheritdoc /> public async Task PostDeploymentComments( CompileJob compileJob, RevisionInformation previousRevisionInformation, RepositorySettings repositorySettings, string repoOwner, string repoName, CancellationToken cancellationToken) { if (repositorySettings?.AccessToken == null) { return; } var deployedRevisionInformation = compileJob.RevisionInformation; if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == deployedRevisionInformation.CommitSha) || !repositorySettings.PostTestMergeComment.Value) { return; } previousRevisionInformation ??= new RevisionInformation(); previousRevisionInformation.ActiveTestMerges ??= new List <RevInfoTestMerge>(); deployedRevisionInformation.ActiveTestMerges ??= new List <RevInfoTestMerge>(); var tasks = new List <Task>(); // added prs var addedTestMerges = deployedRevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => !previousRevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number)) .ToList(); var removedTestMerges = previousRevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => !deployedRevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number)) .ToList(); var updatedTestMerges = deployedRevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => previousRevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number)) .ToList(); if (!addedTestMerges.Any() && !removedTestMerges.Any() && !updatedTestMerges.Any()) { return; } Logger.LogTrace( "Commenting on {0} added, {1} removed, and {2} updated test merge sources...", addedTestMerges.Count, removedTestMerges.Count, updatedTestMerges.Count); foreach (var addedTestMerge in addedTestMerges) { tasks.Add( CommentOnTestMergeSource( repositorySettings, repoOwner, repoName, FormatTestMerge( repositorySettings, compileJob, addedTestMerge, repoOwner, repoName, false), addedTestMerge.Number, cancellationToken)); } foreach (var removedTestMerge in removedTestMerges) { tasks.Add( CommentOnTestMergeSource( repositorySettings, repoOwner, repoName, "#### Test Merge Removed", removedTestMerge.Number, cancellationToken)); } foreach (var updatedTestMerge in updatedTestMerges) { tasks.Add( CommentOnTestMergeSource( repositorySettings, repoOwner, repoName, FormatTestMerge( repositorySettings, compileJob, updatedTestMerge, repoOwner, repoName, true), updatedTestMerge.Number, cancellationToken)); } if (tasks.Any()) { await Task.WhenAll(tasks).ConfigureAwait(false); } }
/// <inheritdoc /> public abstract Task <IReadOnlyCollection <RevInfoTestMerge> > RemoveMergedTestMerges( IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken);
/// <summary> /// Pull the repository and compile for every set of given <paramref name="minutes"/> /// </summary> /// <param name="minutes">How many minutes the operation should repeat. Does not include running time</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task TimerLoop(int minutes, CancellationToken cancellationToken) { while (true) { try { await Task.Delay(new TimeSpan(0, minutes, 0), cancellationToken).ConfigureAwait(false); try { CompileJob job = null; //need this the whole time await databaseContextFactory.UseContext(async (db) => { //start up queries we'll need in the future var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id); var ddSettingsTask = instanceQuery.Select(x => x.DreamDaemonSettings).Select(x => new DreamDaemonSettings { StartupTimeout = x.StartupTimeout, SecurityLevel = x.SecurityLevel }).FirstAsync(cancellationToken); var dmSettingsTask = instanceQuery.Select(x => x.DreamMakerSettings).FirstAsync(cancellationToken); var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstAsync(cancellationToken); using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { return; } //start the rev info query var startSha = repo.Head; var revInfoTask = instanceQuery.SelectMany(x => x.RevisionInformations).Where(x => x.CommitSha == startSha).FirstOrDefaultAsync(cancellationToken); //need repo setting to fetch var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, null, cancellationToken).ConfigureAwait(false); //take appropriate auto update actions bool shouldSyncTracked; if (repositorySettings.AutoUpdatesKeepTestMerges.Value) { var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, cancellationToken).ConfigureAwait(false); if (!result.HasValue) { return; } shouldSyncTracked = result.Value; } else { await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false); shouldSyncTracked = true; } //synch if necessary if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) { await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false); } //finish other queries var dmSettings = await dmSettingsTask.ConfigureAwait(false); var ddSettings = await ddSettingsTask.ConfigureAwait(false); var revInfo = await revInfoTask.ConfigureAwait(false); //null rev info handling if (revInfo == default) { var currentSha = repo.Head; revInfo = new RevisionInformation { CommitSha = currentSha, OriginCommitSha = currentSha, Instance = new Models.Instance { Id = metadata.Id } }; db.Instances.Attach(revInfo.Instance); } //finally start compile job = await DreamMaker.Compile(revInfo, dmSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); } db.CompileJobs.Add(job); await db.Save(cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); await CompileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception e) { logger.LogWarning("Error in auto update loop! Exception: {0}", e); continue; } } catch (OperationCanceledException) { break; } } }
/// <inheritdoc /> public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action <int> progressReporter, CancellationToken cancellationToken) { #pragma warning disable IDE0016 // Use 'throw' expression if (job == null) { throw new ArgumentNullException(nameof(job)); } #pragma warning restore IDE0016 // Use 'throw' expression if (databaseContext == null) { throw new ArgumentNullException(nameof(databaseContext)); } if (progressReporter == null) { throw new ArgumentNullException(nameof(progressReporter)); } var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings { StartupTimeout = x.StartupTimeout, }).FirstOrDefaultAsync(cancellationToken); var compileJobsTask = databaseContext.CompileJobs .Where(x => x.Job.Instance.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) .Select(x => new Job { StoppedAt = x.Job.StoppedAt, StartedAt = x.Job.StartedAt }) .Take(10) .ToListAsync(cancellationToken); var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); if (dreamMakerSettings == default) { throw new JobException("Missing DreamMakerSettings in DB!"); } var ddSettings = await ddSettingsTask.ConfigureAwait(false); if (ddSettings == default) { throw new JobException("Missing DreamDaemonSettings in DB!"); } Task <RepositorySettings> repositorySettingsTask = null; string repoOwner = null; string repoName = null; CompileJob compileJob; RevisionInformation revInfo; using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { throw new JobException("Missing Repository!"); } if (repo.IsGitHubRepository) { repoOwner = repo.GitHubOwner; repoName = repo.GitHubRepoName; repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings { AccessToken = x.AccessToken, ShowTestMergeCommitters = x.ShowTestMergeCommitters, PushTestMergeCommits = x.PushTestMergeCommits }).FirstOrDefaultAsync(cancellationToken); } var repoSha = repo.Head; revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false); if (revInfo == default) { revInfo = new RevisionInformation { CommitSha = repoSha, OriginCommitSha = repoSha, Instance = new Models.Instance { Id = metadata.Id } }; logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); databaseContext.Instances.Attach(revInfo.Instance); } TimeSpan?averageSpan = null; var previousCompileJobs = await compileJobsTask.ConfigureAwait(false); if (previousCompileJobs.Count != 0) { var totalSpan = TimeSpan.Zero; foreach (var I in previousCompileJobs) { totalSpan += I.StoppedAt.Value - I.StartedAt.Value; } averageSpan = totalSpan / previousCompileJobs.Count; } compileJob = await dreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false); } compileJob.Job = job; databaseContext.CompileJobs.Add(compileJob); // will be saved by job context job.PostComplete = ct => compileJobConsumer.LoadCompileJob(compileJob, ct); if (repositorySettingsTask != null) { var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); if (repositorySettings == default) { throw new JobException("Missing repository settings!"); } if (repositorySettings.AccessToken != null) { // potential for commenting on a test merge change var outgoingCompileJob = LatestCompileJob(); if (outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha && repositorySettings.PostTestMergeComment.Value) { var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); async Task CommentOnPR(int prNumber, string comment) { try { await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false); } catch (ApiException e) { logger.LogWarning("Error posting GitHub comment! Exception: {0}", e); } } var tasks = new List <Task>(); string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}", Environment.NewLine, repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty, testMerge.PullRequestRevision, testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty, updated ? "Updated" : "Deployed", metadata.Name, compileJob.RevisionInformation.OriginCommitSha, compileJob.RevisionInformation.CommitSha); // added prs foreach (var I in compileJob .RevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => !outgoingCompileJob .RevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number))) { tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false))); } // removed prs foreach (var I in outgoingCompileJob .RevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => !compileJob .RevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number))) { tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed")); } // updated prs foreach (var I in compileJob .RevisionInformation .ActiveTestMerges .Select(x => x.TestMerge) .Where(x => outgoingCompileJob .RevisionInformation .ActiveTestMerges .Any(y => y.TestMerge.Number == x.Number))) { tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true))); } if (tasks.Any()) { await Task.WhenAll(tasks).ConfigureAwait(false); } } } } }
/// <summary> /// Pull the repository and compile for every set of given <paramref name="minutes"/> /// </summary> /// <param name="minutes">How many minutes the operation should repeat. Does not include running time</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> #pragma warning disable CA1502 // TODO: Decomplexify async Task TimerLoop(uint minutes, CancellationToken cancellationToken) { while (true) { try { await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false); logger.LogInformation("Beginning auto update..."); await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List <string>(), cancellationToken).ConfigureAwait(false); try { Models.User user = null; await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); var repositoryUpdateJob = new Job { Instance = new Models.Instance { Id = metadata.Id }, Description = "Scheduled repository update", CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, StartedBy = user }; string deploySha = null; await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) => { var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); // assume 5 steps with synchronize const int ProgressSections = 7; const int ProgressStep = 100 / ProgressSections; const int NumSteps = 3; var doneSteps = 0; Action <int> NextProgressReporter() { var tmpDoneSteps = doneSteps; ++doneSteps; return(progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps)); } using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false)) { if (repo == null) { logger.LogTrace("Aborting repo update, no repository!"); return; } var startSha = repo.Head; if (!repo.Tracking) { logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!"); deploySha = startSha; return; } var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); // the main point of auto update is to pull the remote await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); RevisionInformation currentRevInfo = null; bool hasDbChanges = false; Task <RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) .FirstOrDefaultAsync(cancellationToken); async Task UpdateRevInfo(string currentHead, bool onOrigin) { if (currentRevInfo == null) { currentRevInfo = await LoadRevInfo().ConfigureAwait(false); } if (currentRevInfo == default) { logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead); onOrigin = true; } var attachedInstance = new Models.Instance { Id = metadata.Id }; var oldRevInfo = currentRevInfo; currentRevInfo = new RevisionInformation { CommitSha = currentHead, OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha, Instance = attachedInstance }; if (!onOrigin) { currentRevInfo.ActiveTestMerges = new List <RevInfoTestMerge>(oldRevInfo.ActiveTestMerges); } databaseContext.Instances.Attach(attachedInstance); databaseContext.RevisionInformations.Add(currentRevInfo); hasDbChanges = true; } // take appropriate auto update actions bool shouldSyncTracked; if (repositorySettings.AutoUpdatesKeepTestMerges.Value) { logger.LogTrace("Preserving test merges..."); var currentRevInfoTask = LoadRevInfo(); var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); if (!result.HasValue) { throw new JobException("Merge conflict while preserving test merges!"); } currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; var currentHead = repo.Head; if (currentHead != startSha) { await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); shouldSyncTracked = stillOnOrigin; } else { shouldSyncTracked = false; } } else { logger.LogTrace("Not preserving test merges..."); await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; currentRevInfo = await databaseContext.RevisionInformations .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); if (currentHead != startSha && currentRevInfo != default) { await UpdateRevInfo(currentHead, true).ConfigureAwait(false); } shouldSyncTracked = true; } // synch if necessary if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) { var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); var currentHead = repo.Head; if (currentHead != currentRevInfo.CommitSha) { await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); } } if (hasDbChanges) { try { await databaseContext.Save(cancellationToken).ConfigureAwait(false); } catch { await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); throw; } } progressReporter(5 * ProgressStep); deploySha = repo.Head; } }, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); if (deploySha == null) { logger.LogTrace("Aborting auto update, repository error!"); continue; } if (deploySha == LatestCompileJob()?.RevisionInformation.CommitSha) { logger.LogTrace("Aborting auto update, same revision as latest CompileJob"); continue; } // finally set up the job var compileProcessJob = new Job { StartedBy = user, Instance = repositoryUpdateJob.Instance, Description = "Scheduled code deployment", CancelRightsType = RightsType.DreamMaker, CancelRight = (ulong)DreamMakerRights.CancelCompile }; await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false); await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false); } catch (OperationCanceledException) { logger.LogDebug("Cancelled auto update job!"); throw; } catch (Exception e) { logger.LogWarning("Error in auto update loop! Exception: {0}", e); continue; } } catch (OperationCanceledException) { break; } } logger.LogTrace("Leaving auto update loop..."); }
/// <inheritdoc /> public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action <int> progressReporter, CancellationToken cancellationToken) { //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE if (job == null) { throw new ArgumentNullException(nameof(job)); } if (databaseContext == null) { throw new ArgumentNullException(nameof(databaseContext)); } if (progressReporter == null) { throw new ArgumentNullException(nameof(progressReporter)); } var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings { StartupTimeout = x.StartupTimeout, }).FirstOrDefaultAsync(cancellationToken); var compileJobsTask = databaseContext.CompileJobs .Where(x => x.Job.Instance.Id == metadata.Id) .OrderByDescending(x => x.Job.StoppedAt) //TODO: Replace with this select when the issues linked in https://github.com/tgstation/tgstation-server/issues/737 are fixed //.Select(x => x.Job.StoppedAt.Value - x.Job.StartedAt.Value) .Select(x => new Job { StoppedAt = x.Job.StoppedAt, StartedAt = x.Job.StartedAt }) .Take(10) .ToListAsync(cancellationToken); var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); if (dreamMakerSettings == default) { throw new JobException("Missing DreamMakerSettings in DB!"); } var ddSettings = await ddSettingsTask.ConfigureAwait(false); if (ddSettings == default) { throw new JobException("Missing DreamDaemonSettings in DB!"); } CompileJob compileJob; RevisionInformation revInfo; using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo == null) { throw new JobException("Missing Repository!"); } var repoSha = repo.Head; revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); if (revInfo == default) { revInfo = new RevisionInformation { CommitSha = repoSha, OriginCommitSha = repoSha, Instance = new Models.Instance { Id = metadata.Id } }; logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); databaseContext.Instances.Attach(revInfo.Instance); } TimeSpan?averageSpan = null; var previousCompileJobs = await compileJobsTask.ConfigureAwait(false); if (previousCompileJobs.Count != 0) { var totalSpan = TimeSpan.Zero; foreach (var I in previousCompileJobs) { totalSpan += I.StoppedAt.Value - I.StartedAt.Value; } averageSpan = totalSpan / previousCompileJobs.Count; } compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false); } compileJob.Job = job; databaseContext.CompileJobs.Add(compileJob); //will be saved by job context job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct); }