Skip to content

Commit

Permalink
feat: cache conclusion and stats in database
Browse files Browse the repository at this point in the history
  • Loading branch information
gregberge committed Dec 31, 2024
1 parent e255b35 commit f5f7ee5
Show file tree
Hide file tree
Showing 24 changed files with 330 additions and 229 deletions.
19 changes: 19 additions & 0 deletions apps/backend/db/migrations/20241231154644_build-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param {import('knex').Knex} knex
*/
export const up = async (knex) => {
await knex.schema.alterTable("builds", async (table) => {
table.enum("conclusion", ["no-changes", "changes-detected"]);
table.jsonb("stats");
});
};

/**
* @param {import('knex').Knex} knex
*/
export const down = async (knex) => {
await knex.schema.alterTable("builds", async (table) => {
table.dropColumn("conclusion");
table.dropColumn("stats");
});
};
6 changes: 5 additions & 1 deletion apps/backend/db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,10 @@ CREATE TABLE public.builds (
metadata jsonb,
"baseBranchResolvedFrom" text,
"parentCommits" jsonb,
conclusion text,
stats jsonb,
CONSTRAINT "builds_baseBranchResolvedFrom_check" CHECK (("baseBranchResolvedFrom" = ANY (ARRAY['user'::text, 'pull-request'::text, 'project'::text]))),
CONSTRAINT builds_conclusion_check CHECK ((conclusion = ANY (ARRAY['no-changes'::text, 'changes-detected'::text]))),
CONSTRAINT builds_mode_check CHECK ((mode = ANY (ARRAY['ci'::text, 'monitoring'::text]))),
CONSTRAINT builds_type_check CHECK ((type = ANY (ARRAY['reference'::text, 'check'::text, 'orphan'::text])))
);
Expand Down Expand Up @@ -2919,4 +2922,5 @@ INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('2024120
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241201100628_remove-github-access-token.js', 1, NOW());
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241201210158_google-account.js', 1, NOW());
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241201222408_gitlab-last-loggedat.js', 1, NOW());
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241215091232_project-default-role.js', 1, NOW());
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241215091232_project-default-role.js', 1, NOW());
INSERT INTO public.knex_migrations(name, batch, migration_time) VALUES ('20241231154644_build-status.js', 1, NOW());
4 changes: 2 additions & 2 deletions apps/backend/src/api/handlers/finalizeBuilds.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe("finalizeBuilds", () => {
{
id: build.id,
number: 1,
status: "stable",
status: "no-changes",
url: "http://localhost:3000/awesome-team/awesome-project/builds/1",
notification: {
description: "Everything's good!",
Expand Down Expand Up @@ -122,7 +122,7 @@ describe("finalizeBuilds", () => {
{
id: build.id,
number: 1,
status: "stable",
status: "no-changes",
url: "http://localhost:3000/awesome-team/awesome-project/builds/1",
notification: {
description: "Everything's good!",
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/api/schema/primitives/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ function getBuildNotificationTypeFromBuildStatus(
return "diff-accepted";
case "rejected":
return "diff-rejected";
case "diffDetected":
case "changes-detected":
return "diff-detected";
case "pending":
return "queued";
case "progress":
return "progress";
case "stable":
case "no-changes":
return "no-diff-detected";
default:
return null;
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/build-notification/comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ export const getCommentBody = async (props: {
invariant(status, "unknown build status");

const label = getBuildLabel(build.type, status);
const review = status === "diffDetected" ? ` ([Review](${url}))` : "";
const review =
status === "changes-detected" ? ` ([Review](${url}))` : "";
const name = getBuildName({
build,
project: hasMultipleProjects ? build.project : null,
Expand Down
50 changes: 50 additions & 0 deletions apps/backend/src/build/concludeBuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { assertNever } from "@argos/util/assertNever";
import { invariant } from "@argos/util/invariant";

import { job as buildNotificationJob } from "@/build-notification/job.js";
import { transaction } from "@/database/index.js";
import { Build, BuildConclusion, BuildNotification } from "@/database/models";

/**
* Concludes the build by updating the conclusion and the stats.
* Called when all diffs are processed.
*/
export async function concludeBuild(input: { buildId: string }) {
const { buildId } = input;
const statuses = await Build.getScreenshotDiffsStatuses([buildId]);
const [[conclusion], [stats]] = await Promise.all([
Build.getConclusions([buildId], statuses),
Build.getStats([buildId]),
]);
invariant(stats !== undefined, "No stats found");
invariant(conclusion !== undefined, "No conclusion found");
// If the build is not yet concluded, we don't want to update it.
if (conclusion === null) {
return;
}
const [, buildNotification] = await transaction(async (trx) => {
return Promise.all([
Build.query(trx).findById(buildId).patch({
conclusion,
stats,
}),
BuildNotification.query(trx).insert({
buildId,
type: getNotificationType(conclusion),
jobStatus: "pending",
}),
]);
});
await buildNotificationJob.push(buildNotification.id);
}

function getNotificationType(conclusion: BuildConclusion) {
switch (conclusion) {
case "changes-detected":
return "diff-detected";
case "no-changes":
return "no-diff-detected";
default:
return assertNever(conclusion);
}
}
5 changes: 1 addition & 4 deletions apps/backend/src/build/createBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ export async function createBuild(params: {
},
);

await pushBuildNotification({
buildId: build.id,
type: "queued",
});
await pushBuildNotification({ buildId: build.id, type: "queued" });

return build;
}
2 changes: 2 additions & 0 deletions apps/backend/src/build/createBuildDiffs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export async function createBuildDiffs(build: Build) {
"no compare screenshot bucket found for build",
);

invariant(compareScreenshotBucket.complete, "compare bucket is not complete");

const compareScreenshots = compareScreenshotBucket.screenshots;
invariant(compareScreenshots, "no compare screenshots found for build");

Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/build/job.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("build", () => {
baseScreenshotBucketId: null,
compareScreenshotBucketId: compareBucket.id,
projectId: project.id,
jobStatus: "pending",
jobStatus: "progress",
});
const file = await factory.File.create({
type: "screenshot",
Expand All @@ -43,6 +43,7 @@ describe("build", () => {
});
await factory.ScreenshotBucket.create({
projectId: project.id,
complete: true,
});
});

Expand Down
30 changes: 9 additions & 21 deletions apps/backend/src/build/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@ import { createModelJob, UnretryableError } from "@/job-core/index.js";
import { job as screenshotDiffJob } from "@/screenshot-diff/index.js";
import { updateStripeUsage } from "@/stripe/index.js";

import { concludeBuild } from "./concludeBuild.js";
import { createBuildDiffs } from "./createBuildDiffs.js";

/**
* Pushes the diffs to the screenshot-diff job queue.
* If there is no diff to proceed, it pushes a notification.
*/
async function pushDiffs(buildId: string, screenshotDiffs: ScreenshotDiff[]) {
if (screenshotDiffs.length === 0) {
await pushBuildNotification({ buildId, type: "no-diff-detected" });
return;
}

const toProcessedDiffIds = screenshotDiffs
async function pushDiffs(input: {
build: Build;
screenshotDiffs: ScreenshotDiff[];
}) {
const toProcessedDiffIds = input.screenshotDiffs
.filter(({ jobStatus }) => jobStatus !== "complete")
.map(({ id }) => id);

Expand All @@ -29,18 +28,7 @@ async function pushDiffs(buildId: string, screenshotDiffs: ScreenshotDiff[]) {
return;
}

await ScreenshotDiff.fetchGraph(screenshotDiffs, "compareScreenshot");
const statuses = await Promise.all(
screenshotDiffs.map((screenshotDiff) => screenshotDiff.$getDiffStatus()),
);

const type = statuses.some(
(status) => status === "added" || status === "removed",
)
? ("diff-detected" as const)
: ("no-diff-detected" as const);

await pushBuildNotification({ buildId, type });
await concludeBuild({ buildId: input.build.id });
}

/**
Expand Down Expand Up @@ -109,8 +97,8 @@ export async function performBuild(build: Build) {
.withGraphFetched("[gitlabProject, account]")
.throwIfNotFound(),
pushBuildNotification({ buildId: build.id, type: "progress" }),
createBuildDiffs(build).then(async (diffs) => {
pushDiffs(build.id, diffs);
createBuildDiffs(build).then(async (screenshotDiffs) => {
await pushDiffs({ build, screenshotDiffs });
}),
]);

Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/build/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function getBuildStatusLabel(status: BuildAggregatedStatus): string {
return "👍 Changes approved";
case "aborted":
return "🙅 Build aborted";
case "diffDetected":
case "changes-detected":
return "⚠️ Changes detected";
case "error":
return "❌ An error happened";
Expand All @@ -23,7 +23,7 @@ function getBuildStatusLabel(status: BuildAggregatedStatus): string {
return "🚜 Diffing screenshots";
case "rejected":
return "👎 Changes rejected";
case "stable":
case "no-changes":
return "✅ No changes detected";
default:
assertNever(status);
Expand Down
12 changes: 6 additions & 6 deletions apps/backend/src/database/models/Build.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,35 +233,35 @@ describe("models/Build", () => {
expect(conclusions).toEqual([null, null, null, null]);
});

it("should return 'stable' when empty", async () => {
it("should return 'no-changes' when empty", async () => {
const build = await factory.Build.create({
jobStatus: "complete",
});
const statuses = await Build.getStatuses([build]);
const conclusions = await Build.getConclusions([build.id], statuses);
expect(conclusions).toEqual(["stable"]);
expect(conclusions).toEqual(["no-changes"]);
});

it("should return 'stable' when no diff detected", async () => {
it("should return 'no-changes' when no diff detected", async () => {
const build = await factory.Build.create();
await factory.ScreenshotDiff.createMany(2, [
{ buildId: build.id },
{ buildId: build.id },
]);
const statuses = await Build.getStatuses([build]);
const conclusions = await Build.getConclusions([build.id], statuses);
expect(conclusions).toEqual(["stable"]);
expect(conclusions).toEqual(["no-changes"]);
});

it("should return 'diff-detected' when diff are detected", async () => {
it("should return 'changes-detected' when diff are detected", async () => {
const build = await factory.Build.create();
await factory.ScreenshotDiff.createMany(2, [
{ buildId: build.id },
{ buildId: build.id, score: 0.8 },
]);
const statuses = await Build.getStatuses([build]);
const conclusions = await Build.getConclusions([build.id], statuses);
expect(conclusions).toEqual(["diffDetected"]);
expect(conclusions).toEqual(["changes-detected"]);
});
});

Expand Down
Loading

0 comments on commit f5f7ee5

Please sign in to comment.