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());
 }
Exemple #2
0
 /// <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);
             }
         }
     }
 }
Exemple #4
0
 /// <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);
Exemple #5
0
        /// <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));
 }
Exemple #11
0
 /// <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);
Exemple #14
0
        /// <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);
        }