private async Task CreateFailedJob(CiBuild build, string failure, CancellationToken cancellationToken) { var outputSection = new CiJobOutputSection() { CiProjectId = build.CiProjectId, CiBuildId = build.CiBuildId, CiJobId = 1, CiJobOutputSectionId = 1, Name = "Invalid configuration", Status = CIJobSectionStatus.Failed, Output = failure }; outputSection.CalculateOutputLength(); var job = new CiJob { CiProjectId = build.CiProjectId, CiBuildId = build.CiBuildId, CiJobId = 1, JobName = "configuration_error", FinishedAt = DateTime.UtcNow, Succeeded = false, State = CIJobState.Finished, CiJobOutputSections = new List <CiJobOutputSection>() { outputSection } }; if (build.CiProject == null) { throw new NotLoadedModelNavigationException(); } await database.CiJobs.AddAsync(job, cancellationToken); await database.CiJobOutputSections.AddAsync(outputSection, cancellationToken); await database.SaveChangesAsync(cancellationToken); if (!await statusReporter.SetCommitStatus(build.CiProject.RepositoryFullName, build.CommitHash, GithubAPI.CommitStatus.Failure, statusReporter.CreateStatusUrlForJob(job), failure, job.JobName)) { logger.LogError("Failed to report serious failed commit status with context {JobName}", job.JobName); } jobClient.Enqueue <CheckOverallBuildStatusJob>(x => x.Execute(build.CiProjectId, build.CiBuildId, CancellationToken.None)); }
public void NotifyAboutBuild(CiBuild build, string statusUrl) { var message = new StringBuilder(100); message.Append(build.CiProject?.Name ?? "unknown project"); message.Append(" build nro "); message.Append(build.CiBuildId); message.Append(" (for: "); message.Append(build.RemoteRef); message.Append(')'); switch (build.Status) { case BuildStatus.Running: message.Append(" is still running"); break; case BuildStatus.Succeeded: message.Append(" has succeeded."); break; case BuildStatus.Failed: message.Append(" has failed"); break; case BuildStatus.GoingToFail: message.Append(" is going to fail"); break; default: throw new ArgumentOutOfRangeException(); } if (build.Status != BuildStatus.Succeeded) { message.Append(". with "); message.Append(build.CiJobs.Count(j => j.Succeeded)); message.Append('/'); message.Append(build.CiJobs.Count); message.Append(" successful jobs."); } message.Append(' '); message.Append(statusUrl); jobClient.Enqueue <SendDiscordWebhookMessageJob>(x => x.Execute("CIBuildNotification", message.ToString(), CancellationToken.None)); }
public async Task <IActionResult> ReceiveWebhook() { var hook = await database.GithubWebhooks.FindAsync(AppInfo.SingleResourceTableRowId); if (hook == null) { logger.LogWarning("Github webhook secret is not configured, can't process webhook"); return(BadRequest("Incorrect secret")); } var payload = await CheckSignature(hook); GithubWebhookContent data; try { data = JsonSerializer.Deserialize <GithubWebhookContent>(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? throw new NullDecodedJsonException(); if (data == null) { throw new Exception("deserialized value is null"); } } catch (Exception e) { logger.LogWarning("Error deserializing github webhook: {@E}", e); throw new HttpResponseException() { Value = new BasicJSONErrorResult("Invalid content", "Failed to deserialize payload").ToString() }; } if (!HttpContext.Request.Headers.TryGetValue("X-GitHub-Event", out StringValues typeHeader) || typeHeader.Count != 1) { throw new HttpResponseException() { Value = new BasicJSONErrorResult("Invalid request", "Missing X-GitHub-Event header").ToString() }; } var type = typeHeader[0]; // TODO: check type on these first two event detections as well if (!string.IsNullOrEmpty(data.Ref) && data.RefType != "branch" && !string.IsNullOrEmpty(data.After)) { // This is a push (commit) logger.LogInformation("Received a push event for ref: {Ref}", data.Ref); if (data.Deleted || data.After == AppInfo.NoCommitHash) { logger.LogInformation("Push was about a deleted thing"); } else { if (data.Before == AppInfo.NoCommitHash) { logger.LogInformation( "Received a push (probably a new branch) with no before set, setting to the after commit"); data.Before = data.After; } if (data.Repository == null) { throw new HttpResponseException() { Value = new BasicJSONErrorResult("Invalid request", "Repository is needed for this event type").ToString(), }; } bool matched = false; // Detect if this triggers any builds foreach (var project in await database.CiProjects.Where(p => p.ProjectType == CIProjectType.Github && p.Enabled && !p.Deleted && p.RepositoryFullName == data.Repository.FullName).ToListAsync()) { matched = true; // Detect next id var previousBuildId = await database.CiBuilds.Where(b => b.CiProjectId == project.Id) .MaxAsync(b => (long?)b.CiBuildId) ?? 0; var build = new CiBuild() { CiProjectId = project.Id, CiBuildId = ++previousBuildId, CommitHash = data.After, RemoteRef = data.Ref, Branch = GitRunHelpers.ParseRefBranch(data.Ref), IsSafe = !GitRunHelpers.IsPullRequestRef(data.Ref), PreviousCommit = data.Before, CommitMessage = data.HeadCommit?.Message ?? data.Commits?.FirstOrDefault()?.Message, ParsedCommits = data.Commits, }; await database.CiBuilds.AddAsync(build); await database.SaveChangesAsync(); jobClient.Enqueue <CheckAndStartCIBuild>(x => x.Execute(build.CiProjectId, build.CiBuildId, CancellationToken.None)); } if (!matched) { logger.LogWarning("Push event didn't match any repos: {Fullname}", data.Repository.FullName); } } } else if (!string.IsNullOrEmpty(data.Ref)) { // This is a branch push (or maybe a tag?) } else if (type == "pull_request") { bool matched = false; if (data.Repository == null) { throw new HttpResponseException() { Value = new BasicJSONErrorResult("Invalid request", "Repository is needed for this event type").ToString(), }; } if (data.PullRequest == null) { throw new HttpResponseException() { Value = new BasicJSONErrorResult("Invalid request", "PullRequest data is needed for this event type").ToString(), }; } jobClient.Enqueue <CheckPullRequestStatusJob>(x => x.Execute(data.Repository.FullName, data.PullRequest.Number, data.PullRequest.Head.Sha, data.PullRequest.User.Login, !data.IsClosedPullRequest, CancellationToken.None)); // Detect if this PR is for any of our repos foreach (var project in await database.CiProjects.Where(p => p.ProjectType == CIProjectType.Github && p.Enabled && !p.Deleted && p.RepositoryFullName == data.Repository.FullName).ToListAsync()) { matched = true; if (data.IsClosedPullRequest) { logger.LogInformation("A pull request was closed"); } else if (data.Action == "synchronize" || data.Action == "opened") { // PR content was changed so we should rebuild (we don't react to other actions to avoid // duplicate builds) // TODO: CLA checks for PRs // Queue a CLA check // Only non-primary repo PRs have CI jobs ran on them as main repo commits trigger // the push event if (data.PullRequest.Head.Repo.Id != data.Repository.Id) { logger.LogInformation("Received pull request event from a fork: {FullName}", data.PullRequest.Head.Repo.FullName); var headRef = GitRunHelpers.GenerateRefForPullRequest(data.PullRequest.Number); // Detect next id var previousBuildId = await database.CiBuilds.Where(b => b.CiProjectId == project.Id) .MaxAsync(b => (long?)b.CiBuildId) ?? 0; var build = new CiBuild() { CiProjectId = project.Id, CiBuildId = ++previousBuildId, CommitHash = data.PullRequest.Head.Sha, RemoteRef = headRef, Branch = GitRunHelpers.ParseRefBranch(headRef), IsSafe = false, PreviousCommit = data.PullRequest.Base.Sha, CommitMessage = $"Pull request #{data.PullRequest.Number}", // TODO: commits would need to be retrieved from data.PullRequest.CommitsUrl Commits = null, }; await database.CiBuilds.AddAsync(build); await database.SaveChangesAsync(); jobClient.Enqueue <CheckAndStartCIBuild>(x => x.Execute(build.CiProjectId, build.CiBuildId, CancellationToken.None)); } // TODO: could run some special actions on PR open if (data.Action == "opened") { } } } if (!matched) { logger.LogWarning("Pull request event didn't match any repos: {Fullname}", data.Repository.FullName); } } // TODO: should this always be updated. Github might send us quite a few events if we subscribe to them all hook.LastUsed = DateTime.UtcNow; await database.SaveChangesAsync(); return(Ok()); }