/// <summary>
		/// Executes a project job, notifying the callback path when complete.
		/// </summary>
		public async Task ExecuteProjectJobAsync(ProjectJob projectJob, string operationId)
		{
			using (_logger.BeginScope(
				new Dictionary<string, object>()
				{
					["OperationId"] = operationId,
					["GitHubOrg"] = projectJob.GitHubOrg,
					["GitHubRepo"] = projectJob.SubmissionRepo,
					["CommitSha"] = projectJob.CommitSha
				}))
			{
				_logger.LogInformation("Starting project job.");

				var result = await RunJobAsync(projectJob, operationId);

				_logger.LogInformation(
					"Project job completed with {status} status and {numTestResults} test results.",
					result.Status.ToString() ?? "Unknown",
					result.TestResults?.Count ?? 0);
				
				await _notifier.NotifyAsync
				(
					_config.ProjectJobResultHost,
					projectJob.CallbackPath,
					operationId,
					result
				);

				_logger.LogInformation("Sent notification for job completion.");
			}
		}
		/// <summary>
		/// Creates a build job for a new commit received by a push event.
		/// Returns the job ID for the build job.
		/// </summary>
		public async Task<string> CreateBuildJobAsync(
			Project project,
			PushEventCommit newCommit,
			string buildResultCallbackUrl)
		{
			var projectJob = new ProjectJob
			(
				newCommit.Commit.BuildRequestToken,
				newCommit.PushEvent.Repository.Owner.Name,
				project.Name,
				newCommit.PushEvent.Repository.Name,
				$"{project.Name}_Template",
				newCommit.Commit.Sha,
				project.PrivateFilePaths
					.Select(p => p.Path)
					.Concat(project.ImmutableFilePaths.Select(p => p.Path))
					.ToList(),
				project.TestClasses
					.Select(tc => tc.ClassName)
					.ToList(),
				buildResultCallbackUrl
			);

			var jobId = await _jobQueueClient.EnqueueAsync<IProjectRunnerService>
			(
				service => service.ExecuteProjectJobAsync
				(
					projectJob,
					_operationIdProvider.OperationId
				)
			);

			return jobId;
		}
		/// <summary>
		/// Runs the job, and returns the result.
		/// </summary>
		private async Task<ProjectJobResult> RunJobAsync(ProjectJob projectJob, string operationId)
		{
			var dockerHost = _dockerHostFactory.CreateDockerHost(c_projectRunnerId);

			var jobStarted = _timeProvider.UtcNow;

			var dockerResult = await dockerHost.RunImageInNewContainerAsync(
				requestContents: null,
				environmentVariables: new Dictionary<string, string>()
				{
					[c_githubOAuthTokenVar] = _config.GitHubOAuthToken,
					[c_githubOrgNameVar] = projectJob.GitHubOrg,
					[c_projectNameVar] = projectJob.ProjectName,
					[c_githubSubmissionRepoNameVar] = projectJob.SubmissionRepo,
					[c_githubTemplateRepoNameVar] = projectJob.TemplateRepo,
					[c_commitShaVar] = projectJob.CommitSha,
					[c_pathsToCopyVar] = string.Join(";", projectJob.CopyPaths),
					[c_testClassesVar] = string.Join(";", projectJob.TestClasses)
				});

			var jobFinished = _timeProvider.UtcNow;

			List<TestResult> testResults = null;
			bool validResponse = dockerResult.Completed
				&& dockerResult.Response != null
				&& TryDeserializeTestResults(dockerResult.Response, out testResults);

			return new ProjectJobResult()
			{
				BuildRequestToken = projectJob.BuildRequestToken,
				Status = GetProjectJobStatus
				(
					dockerResult.Completed, 
					dockerResult.Response != null, 
					validResponse
				),
				JobStartedDate = jobStarted,
				JobFinishedDate = jobFinished,
				BuildOutput = dockerResult.Output,
				TestResults = testResults
			};
		}