public override async Task <IActionResult> Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
        {
            var job = new Models.Job
            {
                Description      = "Compile active repository code",
                StartedBy        = AuthenticationContext.User,
                CancelRightsType = RightsType.DreamMaker,
                CancelRight      = (ulong)DreamMakerRights.CancelCompile,
                Instance         = Instance
            };
            await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false);

            return(Accepted(job.ToApi()));
        }
 /// <inheritdoc />
 public Task <DreamMaker> Update(DreamMaker dreamMaker, CancellationToken cancellationToken) => apiClient.Update <DreamMaker, DreamMaker>(Routes.DreamMaker, dreamMaker, instance.Id, cancellationToken);
Example #3
0
        public async Task <IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
        {
            if (model == null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            if (model.ApiValidationPort == 0)
            {
                throw new InvalidOperationException("ApiValidationPort cannot be 0!");
            }

            var hostModel = await DatabaseContext
                            .DreamMakerSettings
                            .AsQueryable()
                            .Where(x => x.InstanceId == Instance.Id)
                            .FirstOrDefaultAsync(cancellationToken)
                            .ConfigureAwait(false);

            if (hostModel == null)
            {
                return(Gone());
            }

            if (model.ProjectName != null)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme))
                {
                    return(Forbid());
                }
                if (model.ProjectName.Length == 0)
                {
                    hostModel.ProjectName = null;
                }
                else
                {
                    hostModel.ProjectName = model.ProjectName;
                }
            }

            if (model.ApiValidationPort.HasValue)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
                {
                    return(Forbid());
                }
                hostModel.ApiValidationPort = model.ApiValidationPort;
            }

            if (model.ApiValidationSecurityLevel.HasValue)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
                {
                    return(Forbid());
                }
                hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
            }

            if (model.RequireDMApiValidation.HasValue)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationRequirement))
                {
                    return(Forbid());
                }
                hostModel.RequireDMApiValidation = model.RequireDMApiValidation;
            }

            await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);

            if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
            {
                return(NoContent());
            }

            return(await Read(cancellationToken).ConfigureAwait(false));
        }
Example #4
0
        /// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
        public async Task <IInstance> CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata)
        {
            // Create the ioManager for the instance
            var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);

            // various other ioManagers
            var repoIoManager          = new ResolvingIOManager(instanceIoManager, "Repository");
            var byondIOManager         = new ResolvingIOManager(instanceIoManager, "Byond");
            var gameIoManager          = new ResolvingIOManager(instanceIoManager, "Game");
            var diagnosticsIOManager   = new ResolvingIOManager(instanceIoManager, "Diagnostics");
            var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");

            var configuration = new StaticFiles.Configuration(
                configurationIoManager,
                synchronousIOManager,
                symlinkFactory,
                processExecutor,
                postWriteHandler,
                platformIdentifier,
                fileTransferService,
                loggerFactory.CreateLogger <StaticFiles.Configuration>());
            var eventConsumer = new EventConsumer(configuration);
            var repoManager   = new RepositoryManager(
                repositoryFactory,
                repositoryCommands,
                repoIoManager,
                eventConsumer,
                gitRemoteFeaturesFactory,
                loggerFactory.CreateLogger <Repository.Repository>(),
                loggerFactory.CreateLogger <RepositoryManager>());

            try
            {
                var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger <ByondManager>());

                var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, metadata);

                var chatManager = chatFactory.CreateChatManager(instanceIoManager, commandFactory, metadata.ChatSettings);
                try
                {
                    var sessionControllerFactory = new SessionControllerFactory(
                        processExecutor,
                        byond,
                        topicClientFactory,
                        cryptographySuite,
                        assemblyInformationProvider,
                        gameIoManager,
                        chatManager,
                        networkPromptReaper,
                        platformIdentifier,
                        bridgeRegistrar,
                        serverPortProvider,
                        eventConsumer,
                        loggerFactory,
                        loggerFactory.CreateLogger <SessionControllerFactory>(),
                        metadata);

                    var dmbFactory = new DmbFactory(
                        databaseContextFactory,
                        gameIoManager,
                        remoteDeploymentManagerFactory,
                        loggerFactory.CreateLogger <DmbFactory>(),
                        metadata);
                    try
                    {
                        var reattachInfoHandler = new SessionPersistor(
                            databaseContextFactory,
                            dmbFactory,
                            processExecutor,
                            loggerFactory.CreateLogger <SessionPersistor>(),
                            metadata);
                        var watchdog = watchdogFactory.CreateWatchdog(
                            chatManager,
                            dmbFactory,
                            reattachInfoHandler,
                            sessionControllerFactory,
                            gameIoManager,
                            diagnosticsIOManager,
                            eventConsumer,
                            remoteDeploymentManagerFactory,
                            metadata,
                            metadata.DreamDaemonSettings);
                        eventConsumer.SetWatchdog(watchdog);
                        commandFactory.SetWatchdog(watchdog);
                        try
                        {
                            Instance instance   = null;
                            var      dreamMaker = new DreamMaker(
                                byond,
                                gameIoManager,
                                configuration,
                                sessionControllerFactory,
                                eventConsumer,
                                chatManager,
                                processExecutor,
                                dmbFactory,
                                repoManager,
                                remoteDeploymentManagerFactory,
                                loggerFactory.CreateLogger <DreamMaker>(),
                                metadata);

                            instance = new Instance(
                                metadata,
                                repoManager,
                                byond,
                                dreamMaker,
                                watchdog,
                                chatManager,
                                configuration,
                                dmbFactory,
                                jobManager,
                                eventConsumer,
                                remoteDeploymentManagerFactory,
                                loggerFactory.CreateLogger <Instance>());

                            return(instance);
                        }
                        catch
                        {
                            await watchdog.DisposeAsync().ConfigureAwait(false);

                            throw;
                        }
                    }
                    catch
                    {
                        dmbFactory.Dispose();
                        throw;
                    }
                }
                catch
                {
                    await chatManager.DisposeAsync().ConfigureAwait(false);

                    throw;
                }
            }
            catch
            {
                repoManager.Dispose();
                throw;
            }
        }
        /// <inheritdoc />
        public IInstance CreateInstance(Models.Instance metadata)
        {
            //Create the ioManager for the instance
            var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);

            //various other ioManagers
            var repoIoManager          = new ResolvingIOManager(instanceIoManager, "Repository");
            var byondIOManager         = new ResolvingIOManager(instanceIoManager, "Byond");
            var gameIoManager          = new ResolvingIOManager(instanceIoManager, "Game");
            var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");

            var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, loggerFactory.CreateLogger <StaticFiles.Configuration>());
            var eventConsumer = new EventConsumer(configuration);

            var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger <DmbFactory>(), metadata.CloneMetadata());

            try
            {
                var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger <Repository.Repository>(), loggerFactory.CreateLogger <RepositoryManager>());
                try
                {
                    var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger <ByondManager>());

                    var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata);

                    var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings);
                    try
                    {
                        var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, loggerFactory, metadata.CloneMetadata());
                        var reattachInfoHandler      = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger <ReattachInfoHandler>(), metadata.CloneMetadata());
                        var watchdog = watchdogFactory.CreateWatchdog(chat, dmbFactory, reattachInfoHandler, configuration, sessionControllerFactory, metadata.CloneMetadata(), metadata.DreamDaemonSettings);
                        eventConsumer.SetWatchdog(watchdog);
                        commandFactory.SetWatchdog(watchdog);
                        try
                        {
                            var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger <DreamMaker>());

                            return(new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger <Instance>()));
                        }
                        catch
                        {
                            watchdog.Dispose();
                            throw;
                        }
                    }
                    catch
                    {
                        chat.Dispose();
                        throw;
                    }
                }
                catch
                {
                    repoManager.Dispose();
                    throw;
                }
            }
            catch
            {
                dmbFactory.Dispose();
                throw;
            }
        }
        public async Task <IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
        {
            if (model == null)
            {
                throw new ArgumentNullException(nameof(model));
            }

            if (model.ApiValidationPort == 0)
            {
                return(BadRequest(new ErrorMessage {
                    Message = "API Validation port cannot be 0!"
                }));
            }

            if (model.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
            {
                return(BadRequest(new ErrorMessage {
                    Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!"
                }));
            }

            var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);

            if (hostModel == null)
            {
                return(StatusCode((int)HttpStatusCode.Gone));
            }

            if (model.ProjectName != null)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetDme))
                {
                    return(Forbid());
                }
                if (model.ProjectName.Length == 0)
                {
                    hostModel.ProjectName = null;
                }
                else
                {
                    hostModel.ProjectName = model.ProjectName;
                }
            }

            if (model.ApiValidationPort.HasValue)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
                {
                    return(Forbid());
                }
                hostModel.ApiValidationPort = model.ApiValidationPort;
            }

            if (model.ApiValidationSecurityLevel.HasValue)
            {
                if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
                {
                    return(Forbid());
                }
                hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
            }

            await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);

            if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
            {
                return(NoContent());
            }

            return(await Read(cancellationToken).ConfigureAwait(false));
        }
Example #7
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;
                }
            }
        }
Example #8
0
        /// <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);
        }
Example #9
0
        /// <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!");
            }

            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
                    }).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)
                    {
                        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);
                        }
                    }
                }
            }
        }