diff --git a/apps/backend/src/build-notification/comment.ts b/apps/backend/src/build-notification/comment.ts index 36ef793a5..5dde57de2 100644 --- a/apps/backend/src/build-notification/comment.ts +++ b/apps/backend/src/build-notification/comment.ts @@ -28,9 +28,9 @@ function getBuildName(input: { build: Build; project: Project | null }) { return input.build.name; } -export const getCommentBody = async (props: { +export async function getCommentBody(props: { commit: string; -}): Promise => { +}): Promise { const builds = await Build.query() .distinctOn("builds.name", "builds.projectId") .joinRelated("compareScreenshotBucket") @@ -44,40 +44,40 @@ export const getCommentBody = async (props: { const hasMultipleProjects = new Set(builds.map((build) => build.projectId)).size > 1; - const aggregateStatuses = await Build.getAggregatedBuildStatuses(builds); - const buildRows = await Promise.all( - builds - // Filter out builds that are not associated with a project that has PR comments enabled. - // We don't filter at SQL level because we still want to know if it's linked to multiple projects - // to determine if we should prepend the project name to the build name. - .filter((build) => { - invariant(build.project, "Relation `project` should be fetched"); - return build.project.prCommentEnabled; - }) - .map(async (build, index) => { - invariant(build.project, "Relation `project` should be fetched"); + const [aggregateStatuses, urls] = await Promise.all([ + Build.getAggregatedBuildStatuses(builds), + Promise.all(builds.map((build) => build.getUrl())), + ]); + const buildRows = builds + // Filter out builds that are not associated with a project that has PR comments enabled. + // We don't filter at SQL level because we still want to know if it's linked to multiple projects + // to determine if we should prepend the project name to the build name. + .filter((build) => { + invariant(build.project, "Relation `project` should be fetched"); + return build.project.prCommentEnabled; + }) + .map((build, index) => { + invariant(build.project, "Relation `project` should be fetched"); - const [stats, url] = await Promise.all([ - getStatsMessage(build.id), - build.getUrl(), - ]); + const url = urls[index]; + invariant(url, "missing build URL"); - const status = aggregateStatuses[index]; - invariant(status, "unknown build status"); + const status = aggregateStatuses[index]; + invariant(status, "missing build status"); - const label = getBuildLabel(build.type, status); - const review = - status === "changes-detected" ? ` ([Review](${url}))` : ""; - const name = getBuildName({ - build, - project: hasMultipleProjects ? build.project : null, - }); + const stats = build.stats ? getStatsMessage(build.stats) : null; - return `| **${name}** ([Inspect](${url})) | ${label}${review} | ${ - stats || "-" - } | ${formatDate(build.updatedAt)} |`; - }), - ); + const label = getBuildLabel(build.type, status); + const review = status === "changes-detected" ? ` ([Review](${url}))` : ""; + const name = getBuildName({ + build, + project: hasMultipleProjects ? build.project : null, + }); + + return `| **${name}** ([Inspect](${url})) | ${label}${review} | ${ + stats || "-" + } | ${formatDate(build.updatedAt)} |`; + }); return [ getCommentHeader(), "", @@ -85,4 +85,4 @@ export const getCommentBody = async (props: { `| :---- | :----- | :------ | :------------ |`, ...buildRows.sort(), ].join("\n"); -}; +} diff --git a/apps/backend/src/build-notification/notification.ts b/apps/backend/src/build-notification/notification.ts index 2c0796c1a..0daea5e70 100644 --- a/apps/backend/src/build-notification/notification.ts +++ b/apps/backend/src/build-notification/notification.ts @@ -123,23 +123,28 @@ export function getNotificationStates(input: { } } +function getBuildStatsMessage(build: Build): string { + invariant(build.stats, "Build should be concluded"); + return getStatsMessage(build.stats); +} + /** * Get the notification description for each platform based on the build * notification type and if it's a auto-approved build. */ -async function getNotificationDescription(input: { +function getNotificationDescription(input: { buildNotificationType: BuildNotification["type"]; - buildId: string; + build: Build; isAutoApproved: boolean; -}): Promise { - const { buildNotificationType, buildId, isAutoApproved } = input; +}): string { + const { buildNotificationType, build, isAutoApproved } = input; switch (buildNotificationType) { case "queued": return "Build is queued"; case "progress": return "Build in progress..."; case "no-diff-detected": { - const statsMessage = await getStatsMessage(buildId); + const statsMessage = getBuildStatsMessage(build); if (!statsMessage) { if (isAutoApproved) { return "Auto-approved, no changes found"; @@ -152,18 +157,18 @@ async function getNotificationDescription(input: { return `${statsMessage} — no changes found`; } case "diff-detected": { - const statsMessage = await getStatsMessage(buildId); + const statsMessage = getBuildStatsMessage(build); if (isAutoApproved) { return `${statsMessage}, automatically approved`; } return `${statsMessage} — waiting for your decision`; } case "diff-accepted": { - const statsMessage = await getStatsMessage(buildId); + const statsMessage = getBuildStatsMessage(build); return `${statsMessage} — approved`; } case "diff-rejected": { - const statsMessage = await getStatsMessage(buildId); + const statsMessage = getBuildStatsMessage(build); return `${statsMessage} — rejected`; } default: { @@ -179,18 +184,16 @@ export async function getNotificationPayload(input: { buildNotification: Pick; build: Build; }): Promise { - const [description, context] = await Promise.all([ - getNotificationDescription({ - buildNotificationType: input.buildNotification.type, - buildId: input.build.id, - isAutoApproved: input.build.type === "reference", - }), - getStatusContext(input.build), - ]); + const description = getNotificationDescription({ + buildNotificationType: input.buildNotification.type, + build: input.build, + isAutoApproved: input.build.type === "reference", + }); const states = getNotificationStates({ buildNotificationType: input.buildNotification.type, isAutoApproved: input.build.type === "reference", }); + const context = await getStatusContext(input.build); return { description, diff --git a/apps/backend/src/build/concludeBuild.ts b/apps/backend/src/build/concludeBuild.ts index 62ebcf0ea..684258376 100644 --- a/apps/backend/src/build/concludeBuild.ts +++ b/apps/backend/src/build/concludeBuild.ts @@ -13,8 +13,8 @@ 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]), + Build.computeConclusions([buildId], statuses), + Build.computeStats([buildId]), ]); invariant(stats !== undefined, "No stats found"); invariant(conclusion !== undefined, "No conclusion found"); diff --git a/apps/backend/src/build/stats.ts b/apps/backend/src/build/stats.ts index ef426bf79..ac86defe6 100644 --- a/apps/backend/src/build/stats.ts +++ b/apps/backend/src/build/stats.ts @@ -1,5 +1,3 @@ -import { invariant } from "@argos/util/invariant"; - import { Build } from "@/database/models/index.js"; /** @@ -7,9 +5,7 @@ import { Build } from "@/database/models/index.js"; * Example: 40 changed, 20 added, 10 removed, 5 failures * Around 45 characters max. */ -export async function getStatsMessage(buildId: string): Promise { - const [stats] = await Build.getStats([buildId]); - invariant(stats, "Build stats not found"); +export function getStatsMessage(stats: NonNullable): string { const parts = []; if (stats.changed > 0) { parts.push(`${stats.changed} changed`); diff --git a/apps/backend/src/database/models/Build.e2e.test.ts b/apps/backend/src/database/models/Build.e2e.test.ts index 93f363828..8ca742aab 100644 --- a/apps/backend/src/database/models/Build.e2e.test.ts +++ b/apps/backend/src/database/models/Build.e2e.test.ts @@ -94,20 +94,25 @@ describe("models/Build", () => { }); }); - describe("#getStatus", () => { + describe("#getStatuses", () => { let build; + const getBuildStatus = async (build: Build) => { + const [status] = await Build.getStatuses([build]); + return status; + }; + describe("with in progress job", () => { it("should be pending", async () => { build = await factory.Build.create({ jobStatus: "progress" }); - expect(await build.$getStatus()).toBe("pending"); + expect(await getBuildStatus(build)).toBe("pending"); }); }); describe("with pending job", () => { it("should be pending", async () => { build = await factory.Build.create({ jobStatus: "pending" }); - expect(await build.$getStatus()).toBe("pending"); + expect(await getBuildStatus(build)).toBe("pending"); }); }); @@ -119,7 +124,7 @@ describe("models/Build", () => { new Date().valueOf() - 3 * 3600 * 1000, ).toISOString(), }); - expect(await build.$getStatus()).toBe("expired"); + expect(await getBuildStatus(build)).toBe("expired"); }); }); @@ -131,7 +136,7 @@ describe("models/Build", () => { new Date().valueOf() - 3 * 3600 * 1000, ).toISOString(), }); - expect(await build.$getStatus()).toBe("expired"); + expect(await getBuildStatus(build)).toBe("expired"); }); }); @@ -143,7 +148,7 @@ describe("models/Build", () => { { buildId: build.id, jobStatus: "complete" }, { buildId: build.id, jobStatus: "error" }, ]); - expect(await build.$getStatus()).toBe("error"); + expect(await getBuildStatus(build)).toBe("error"); }); }); @@ -154,7 +159,7 @@ describe("models/Build", () => { { buildId: build.id, jobStatus: "complete" }, { buildId: build.id, jobStatus: "pending" }, ]); - expect(await build.$getStatus()).toBe("progress"); + expect(await getBuildStatus(build)).toBe("progress"); }); }); @@ -165,7 +170,7 @@ describe("models/Build", () => { { buildId: build.id, jobStatus: "complete" }, { buildId: build.id, jobStatus: "progress" }, ]); - expect(await build.$getStatus()).toBe("progress"); + expect(await getBuildStatus(build)).toBe("progress"); }); }); @@ -176,7 +181,7 @@ describe("models/Build", () => { { buildId: build.id, jobStatus: "complete" }, { buildId: build.id, jobStatus: "complete" }, ]); - expect(await build.$getStatus()).toBe("complete"); + expect(await getBuildStatus(build)).toBe("complete"); }); }); }); @@ -184,14 +189,14 @@ describe("models/Build", () => { describe("with aborted job", () => { it("should be aborted", async () => { build = await factory.Build.create({ jobStatus: "aborted" }); - expect(await build.$getStatus()).toBe("aborted"); + expect(await getBuildStatus(build)).toBe("aborted"); }); }); describe("with error job", () => { it("should be error", async () => { build = await factory.Build.create({ jobStatus: "error" }); - expect(await build.$getStatus()).toBe("error"); + expect(await getBuildStatus(build)).toBe("error"); }); }); }); @@ -216,7 +221,7 @@ describe("models/Build", () => { }); }); - describe("#getConclusions", () => { + describe("#computeConclusions", () => { it("should return null for uncompleted jobs", async () => { const builds = await factory.Build.createMany(4, [ { jobStatus: "pending" }, @@ -226,7 +231,7 @@ describe("models/Build", () => { ]); const statuses = await Build.getStatuses(builds); - const conclusions = await Build.getConclusions( + const conclusions = await Build.computeConclusions( builds.map((b) => b.id), statuses, ); @@ -238,7 +243,7 @@ describe("models/Build", () => { jobStatus: "complete", }); const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); + const conclusions = await Build.computeConclusions([build.id], statuses); expect(conclusions).toEqual(["no-changes"]); }); @@ -249,7 +254,7 @@ describe("models/Build", () => { { buildId: build.id }, ]); const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); + const conclusions = await Build.computeConclusions([build.id], statuses); expect(conclusions).toEqual(["no-changes"]); }); @@ -260,7 +265,7 @@ describe("models/Build", () => { { buildId: build.id, score: 0.8 }, ]); const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); + const conclusions = await Build.computeConclusions([build.id], statuses); expect(conclusions).toEqual(["changes-detected"]); }); }); @@ -268,75 +273,55 @@ describe("models/Build", () => { describe("#reviewStatuses", () => { it("should return null for uncompleted jobs", async () => { const build = await factory.Build.create({ - jobStatus: "pending", + conclusion: null, }); - const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); - const reviewStatuses = await Build.getReviewStatuses( - [build.id], - conclusions, - ); + const reviewStatuses = await Build.getReviewStatuses([build]); expect(reviewStatuses).toEqual([null]); }); - it("should return null for stable build", async () => { - const builds = await factory.Build.createMany(2); + it("should return null for 'no-changes' builds", async () => { + const builds = await factory.Build.createMany(2, { + conclusion: "no-changes", + }); await factory.ScreenshotDiff.createMany(2, { buildId: builds[0]!.id }); await factory.ScreenshotDiff.createMany(2, { buildId: builds[1]!.id }); - const statuses = await Build.getStatuses(builds); - const conclusions = await Build.getConclusions( - builds.map((b) => b.id), - statuses, - ); - const reviewStatuses = await Build.getReviewStatuses( - builds.map((b) => b.id), - conclusions, - ); + const reviewStatuses = await Build.getReviewStatuses(builds); expect(reviewStatuses).toEqual([null, null]); }); it("should return 'accepted' when all diff are accepted", async () => { - const build = await factory.Build.create(); + const build = await factory.Build.create({ + conclusion: "changes-detected", + }); await factory.ScreenshotDiff.createMany(2, [ { buildId: build.id, score: 0.8, validationStatus: "accepted" }, { buildId: build.id, score: 0.4, validationStatus: "accepted" }, ]); - const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); - const reviewStatuses = await Build.getReviewStatuses( - [build.id], - conclusions, - ); + const reviewStatuses = await Build.getReviewStatuses([build]); expect(reviewStatuses).toEqual(["accepted"]); }); it("should return 'rejected' when one diff is rejected", async () => { - const build = await factory.Build.create(); + const build = await factory.Build.create({ + conclusion: "changes-detected", + }); await factory.ScreenshotDiff.createMany(2, [ { buildId: build.id, score: 0.8, validationStatus: "accepted" }, { buildId: build.id, score: 0.4, validationStatus: "rejected" }, ]); - const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); - const reviewStatuses = await Build.getReviewStatuses( - [build.id], - conclusions, - ); + const reviewStatuses = await Build.getReviewStatuses([build]); expect(reviewStatuses).toEqual(["rejected"]); }); it("should return null in other case", async () => { - const build = await factory.Build.create(); + const build = await factory.Build.create({ + conclusion: "changes-detected", + }); await factory.ScreenshotDiff.createMany(2, [ { buildId: build.id, score: 0.8, validationStatus: "accepted" }, { buildId: build.id, score: 0.4, validationStatus: "unknown" }, ]); - const statuses = await Build.getStatuses([build]); - const conclusions = await Build.getConclusions([build.id], statuses); - const reviewStatuses = await Build.getReviewStatuses( - [build.id], - conclusions, - ); + const reviewStatuses = await Build.getReviewStatuses([build]); expect(reviewStatuses).toEqual([null]); }); }); diff --git a/apps/backend/src/database/models/Build.ts b/apps/backend/src/database/models/Build.ts index 6a12f48f6..d102d9bb9 100644 --- a/apps/backend/src/database/models/Build.ts +++ b/apps/backend/src/database/models/Build.ts @@ -314,13 +314,13 @@ export class Build extends Model { * Get status of the build. * Aggregate statuses from screenshot diffs. */ - static async getStatuses(builds: Build[]) { - const completeBuildIds = builds - .filter(({ jobStatus }) => jobStatus === "complete") + static async getStatuses(builds: Build[]): Promise { + const unconcludedBuilds = builds + .filter((build) => build.jobStatus === "complete" && !build.conclusion) .map(({ id }) => id); const screenshotDiffStatuses = - await Build.getScreenshotDiffsStatuses(completeBuildIds); + await Build.getScreenshotDiffsStatuses(unconcludedBuilds); return builds.map((build) => { switch (build.jobStatus) { @@ -328,15 +328,19 @@ export class Build extends Model { case "progress": return Date.now() - new Date(build.createdAt).getTime() > 2 * 3600 * 1000 - ? "expired" - : "pending"; + ? ("expired" as const) + : ("pending" as const); case "error": case "aborted": return build.jobStatus; case "complete": { - const index = completeBuildIds.indexOf(build.id); + const index = unconcludedBuilds.indexOf(build.id); + // If the build is concluded, we don't need to check the diff status. + if (index === -1) { + return build.jobStatus; + } const diffStatus = screenshotDiffStatuses[index]; invariant(diffStatus, "diff status not found"); return diffStatus; @@ -348,9 +352,9 @@ export class Build extends Model { } /** - * Get stats of the build. + * Compute stats for a list of builds. */ - static async getStats(buildIds: string[]): Promise { + static async computeStats(buildIds: string[]): Promise { const data = (await ScreenshotDiff.query() .whereIn("buildId", buildIds) .leftJoinRelated("compareScreenshot") @@ -387,18 +391,10 @@ export class Build extends Model { }); } - /** - * Get status of the current build. - */ - async $getStatus() { - const statuses = await Build.getStatuses([this]); - return statuses[0]; - } - /** * Get the conclusion of builds. */ - static async getConclusions( + static async computeConclusions( buildIds: string[], statuses: BuildStatus[], ): Promise<(BuildConclusion | null)[]> { @@ -438,12 +434,11 @@ export class Build extends Model { * Get the review status of builds. */ static async getReviewStatuses( - buildIds: string[], - conclusions: (BuildConclusion | null)[], + builds: Build[], ): Promise<(BuildReviewStatus | null)[]> { - const diffDetectedBuildIds = buildIds.filter( - (_buildId, index) => conclusions[index] === "changes-detected", - ); + const diffDetectedBuildIds = builds + .filter((build) => build.conclusion === "changes-detected") + .map((build) => build.id); const screenshotDiffs = diffDetectedBuildIds.length ? await ScreenshotDiff.query() @@ -452,13 +447,13 @@ export class Build extends Model { .groupBy("buildId", "validationStatus") : []; - return buildIds.map((buildId, index) => { - if (conclusions[index] !== "changes-detected") { + return builds.map((build) => { + if (build.conclusion !== "changes-detected") { return null; } const status = screenshotDiffs - .filter((diff) => diff.buildId === buildId) + .filter((diff) => diff.buildId === build.id) .map(({ validationStatus }) => validationStatus); if (status.length === 1 && status[0] === "accepted") { @@ -501,13 +496,13 @@ export class Build extends Model { static async getAggregatedBuildStatuses( builds: Build[], ): Promise { - const buildIds = builds.map((build) => build.id); - const statuses = await Build.getStatuses(builds); - const conclusions = await Build.getConclusions(buildIds, statuses); - const reviewStatuses = await Build.getReviewStatuses(buildIds, conclusions); - return builds.map((_build, index) => { + const [statuses, reviewStatuses] = await Promise.all([ + Build.getStatuses(builds), + Build.getReviewStatuses(builds), + ]); + return builds.map((build, index) => { const status = - reviewStatuses[index] || conclusions[index] || statuses[index]; + reviewStatuses[index] || build.conclusion || statuses[index]; return BuildAggregatedStatusSchema.parse(status); }); } diff --git a/apps/backend/src/graphql/__generated__/resolver-types.ts b/apps/backend/src/graphql/__generated__/resolver-types.ts index 9d3dd33ff..5e6bce17e 100644 --- a/apps/backend/src/graphql/__generated__/resolver-types.ts +++ b/apps/backend/src/graphql/__generated__/resolver-types.ts @@ -205,7 +205,7 @@ export type IBuild = INode & { /** The screenshot diffs between the base screenshot bucket of the compare screenshot bucket */ screenshotDiffs: IScreenshotDiffConnection; /** Build stats */ - stats: IBuildStats; + stats?: Maybe; /** Review status, conclusion or job status */ status: IBuildStatus; /** Build type */ @@ -1702,7 +1702,7 @@ export type IBuildResolvers, ParentType, ContextType>; pullRequest?: Resolver, ParentType, ContextType>; screenshotDiffs?: Resolver>; - stats?: Resolver; + stats?: Resolver, ParentType, ContextType>; status?: Resolver; type?: Resolver, ParentType, ContextType>; updatedAt?: Resolver; diff --git a/apps/backend/src/graphql/__generated__/schema.gql b/apps/backend/src/graphql/__generated__/schema.gql index 551ac7234..69c2e8001 100644 --- a/apps/backend/src/graphql/__generated__/schema.gql +++ b/apps/backend/src/graphql/__generated__/schema.gql @@ -180,7 +180,7 @@ type Build implements Node { screenshotDiffs(after: Int!, first: Int!): ScreenshotDiffConnection! """Build stats""" - stats: BuildStats! + stats: BuildStats """Review status, conclusion or job status""" status: BuildStatus! diff --git a/apps/backend/src/graphql/definitions/Build.ts b/apps/backend/src/graphql/definitions/Build.ts index 25988d51b..5b6404b4f 100644 --- a/apps/backend/src/graphql/definitions/Build.ts +++ b/apps/backend/src/graphql/definitions/Build.ts @@ -94,7 +94,7 @@ export const typeDefs = gql` "Pull request" pullRequest: PullRequest "Build stats" - stats: BuildStats! + stats: BuildStats "Build type" type: BuildType "Pull request head commit" @@ -218,9 +218,6 @@ export const resolvers: IResolvers = { assertNever(status); } }, - stats: async (build, _args, ctx) => { - return ctx.loaders.BuildStats.load(build.id); - }, commit: async (build, _args, ctx) => { if (build.prHeadCommit) { return build.prHeadCommit; diff --git a/apps/backend/src/graphql/loaders.ts b/apps/backend/src/graphql/loaders.ts index 5f04950ce..019ac30d3 100644 --- a/apps/backend/src/graphql/loaders.ts +++ b/apps/backend/src/graphql/loaders.ts @@ -6,7 +6,6 @@ import { Account, Build, BuildAggregatedStatus, - BuildStats, File, GithubAccount, GithubInstallation, @@ -174,12 +173,6 @@ function createBuildFromCompareScreenshotBucketIdLoader() { ); } -function createBuildStatsLoader() { - return new DataLoader(async (buildIds) => { - return Build.getStats(buildIds as string[]); - }); -} - function createGhApiInstallationLoader() { return new DataLoader< { app: GithubInstallation["app"]; installationId: number }, @@ -206,7 +199,6 @@ export const createLoaders = () => ({ BuildFromCompareScreenshotBucketId: createBuildFromCompareScreenshotBucketIdLoader(), BuildAggregatedStatus: createBuildAggregatedStatusLoader(), - BuildStats: createBuildStatsLoader(), File: createModelLoader(File), GhApiInstallation: createGhApiInstallationLoader(), GithubAccount: createModelLoader(GithubAccount), diff --git a/apps/backend/src/slack/index.ts b/apps/backend/src/slack/index.ts index aff00ed2a..785c28c17 100644 --- a/apps/backend/src/slack/index.ts +++ b/apps/backend/src/slack/index.ts @@ -239,9 +239,10 @@ async function unfurlBuild( return null; } - const [[status], statsMessage, screenshotDiff] = await Promise.all([ + const statsMessage = build.stats ? getStatsMessage(build.stats) : null; + + const [[status], screenshotDiff] = await Promise.all([ Build.getAggregatedBuildStatuses([build]), - getStatsMessage(build.id), params.diffId ? ScreenshotDiff.query() .findById(params.diffId) diff --git a/apps/frontend/src/containers/BuildStatusDescription.tsx b/apps/frontend/src/containers/BuildStatusDescription.tsx index 84e4ef775..ae832aa58 100644 --- a/apps/frontend/src/containers/BuildStatusDescription.tsx +++ b/apps/frontend/src/containers/BuildStatusDescription.tsx @@ -1,4 +1,5 @@ import { assertNever } from "@argos/util/assertNever"; +import { invariant } from "@argos/util/invariant"; import { FragmentType, graphql, useFragment } from "@/gql"; import { BuildMode, BuildStatus } from "@/gql/graphql"; @@ -112,6 +113,7 @@ export const BuildStatusDescription = (props: { case "check": { switch (build.status) { case BuildStatus.NoChanges: { + invariant(build.stats, "Concluded build should have stats"); if (build.stats.total === 0) { return ( <> diff --git a/apps/frontend/src/gql/graphql.ts b/apps/frontend/src/gql/graphql.ts index 9000e04a7..908b04723 100644 --- a/apps/frontend/src/gql/graphql.ts +++ b/apps/frontend/src/gql/graphql.ts @@ -200,7 +200,7 @@ export type Build = Node & { /** The screenshot diffs between the base screenshot bucket of the compare screenshot bucket */ screenshotDiffs: ScreenshotDiffConnection; /** Build stats */ - stats: BuildStats; + stats?: Maybe; /** Review status, conclusion or job status */ status: BuildStatus; /** Build type */ @@ -1417,7 +1417,7 @@ export type BuildStatusChip_BuildFragment = ( & { ' $fragmentRefs'?: { 'BuildStatusDescription_BuildFragment': BuildStatusDescription_BuildFragment } } ) & { ' $fragmentName'?: 'BuildStatusChip_BuildFragment' }; -export type BuildStatusDescription_BuildFragment = { __typename?: 'Build', type?: BuildType | null, status: BuildStatus, mode: BuildMode, baseBranch?: string | null, stats: { __typename?: 'BuildStats', total: number }, parallel?: { __typename?: 'BuildParallel', total: number, received: number, nonce: string } | null } & { ' $fragmentName'?: 'BuildStatusDescription_BuildFragment' }; +export type BuildStatusDescription_BuildFragment = { __typename?: 'Build', type?: BuildType | null, status: BuildStatus, mode: BuildMode, baseBranch?: string | null, stats?: { __typename?: 'BuildStats', total: number } | null, parallel?: { __typename?: 'BuildParallel', total: number, received: number, nonce: string } | null } & { ' $fragmentName'?: 'BuildStatusDescription_BuildFragment' }; export type GithubAccountLink_GithubAccountFragment = { __typename?: 'GithubAccount', login: string, name?: string | null, url: string } & { ' $fragmentName'?: 'GithubAccountLink_GithubAccountFragment' }; @@ -2080,7 +2080,7 @@ export type Account_AccountQuery = { __typename?: 'Query', account?: ( & { ' $fragmentRefs'?: { 'PaymentBanner_Account_User_Fragment': PaymentBanner_Account_User_Fragment } } ) | null }; -export type BuildDetail_BuildFragment = { __typename?: 'Build', id: string, createdAt: any, branch?: string | null, type?: BuildType | null, stats: { __typename?: 'BuildStats', total: number }, baseScreenshotBucket?: { __typename?: 'ScreenshotBucket', id: string, branch?: string | null, createdAt: any } | null, pullRequest?: { __typename?: 'GithubPullRequest', merged?: boolean | null } | null } & { ' $fragmentName'?: 'BuildDetail_BuildFragment' }; +export type BuildDetail_BuildFragment = { __typename?: 'Build', id: string, createdAt: any, branch?: string | null, type?: BuildType | null, stats?: { __typename?: 'BuildStats', total: number } | null, baseScreenshotBucket?: { __typename?: 'ScreenshotBucket', id: string, branch?: string | null, createdAt: any } | null, pullRequest?: { __typename?: 'GithubPullRequest', merged?: boolean | null } | null } & { ' $fragmentName'?: 'BuildDetail_BuildFragment' }; export type BuildDiffState_ScreenshotDiffFragment = { __typename?: 'ScreenshotDiff', id: string, status: ScreenshotDiffStatus, url?: string | null, name: string, width?: number | null, height?: number | null, group?: string | null, threshold?: number | null, baseScreenshot?: { __typename?: 'Screenshot', id: string, url: string, width?: number | null, height?: number | null, metadata?: { __typename?: 'ScreenshotMetadata', url?: string | null, colorScheme?: ScreenshotMetadataColorScheme | null, mediaType?: ScreenshotMetadataMediaType | null, automationLibrary: { __typename?: 'ScreenshotMetadataAutomationLibrary', name: string, version: string }, browser?: { __typename?: 'ScreenshotMetadataBrowser', name: string, version: string } | null, sdk: { __typename?: 'ScreenshotMetadataSDK', name: string, version: string }, viewport?: { __typename?: 'ScreenshotMetadataViewport', width: number, height: number } | null, test?: { __typename?: 'ScreenshotMetadataTest', id?: string | null, title: string, titlePath: Array, retry?: number | null, retries?: number | null, repeat?: number | null, location?: { __typename?: 'ScreenshotMetadataLocation', file: string, line: number } | null } | null } | null } | null, compareScreenshot?: { __typename?: 'Screenshot', id: string, url: string, width?: number | null, height?: number | null, playwrightTraceUrl?: string | null, metadata?: { __typename?: 'ScreenshotMetadata', url?: string | null, colorScheme?: ScreenshotMetadataColorScheme | null, mediaType?: ScreenshotMetadataMediaType | null, automationLibrary: { __typename?: 'ScreenshotMetadataAutomationLibrary', name: string, version: string }, browser?: { __typename?: 'ScreenshotMetadataBrowser', name: string, version: string } | null, sdk: { __typename?: 'ScreenshotMetadataSDK', name: string, version: string }, viewport?: { __typename?: 'ScreenshotMetadataViewport', width: number, height: number } | null, test?: { __typename?: 'ScreenshotMetadataTest', id?: string | null, title: string, titlePath: Array, retry?: number | null, retries?: number | null, repeat?: number | null, location?: { __typename?: 'ScreenshotMetadataLocation', file: string, line: number } | null } | null } | null } | null } & { ' $fragmentName'?: 'BuildDiffState_ScreenshotDiffFragment' }; @@ -2098,12 +2098,12 @@ export type BuildDiffState_ProjectQuery = { __typename?: 'Query', project?: { __ & { ' $fragmentRefs'?: { 'BuildDiffState_ScreenshotDiffFragment': BuildDiffState_ScreenshotDiffFragment } } )> } } | null } | null }; -export type BuildDiffState_BuildFragment = { __typename?: 'Build', id: string, stats: ( +export type BuildDiffState_BuildFragment = { __typename?: 'Build', id: string, stats?: ( { __typename?: 'BuildStats', total: number, failure: number, changed: number, added: number, removed: number, unchanged: number, retryFailure: number } & { ' $fragmentRefs'?: { 'BuildStatsIndicator_BuildStatsFragment': BuildStatsIndicator_BuildStatsFragment } } - ) } & { ' $fragmentName'?: 'BuildDiffState_BuildFragment' }; + ) | null } & { ' $fragmentName'?: 'BuildDiffState_BuildFragment' }; -export type BuildInfos_BuildFragment = { __typename?: 'Build', createdAt: any, name: string, commit: string, branch?: string | null, mode: BuildMode, baseBranch?: string | null, baseBranchResolvedFrom?: BaseBranchResolution | null, stats: { __typename?: 'BuildStats', total: number }, baseScreenshotBucket?: { __typename?: 'ScreenshotBucket', id: string, commit: string, branch?: string | null } | null, baseBuild?: { __typename?: 'Build', id: string, number: number } | null, pullRequest?: { __typename?: 'GithubPullRequest', id: string, url: string, number: number } | null, metadata?: { __typename?: 'BuildMetadata', testReport?: { __typename?: 'TestReport', status: TestReportStatus } | null } | null } & { ' $fragmentName'?: 'BuildInfos_BuildFragment' }; +export type BuildInfos_BuildFragment = { __typename?: 'Build', createdAt: any, name: string, commit: string, branch?: string | null, mode: BuildMode, baseBranch?: string | null, baseBranchResolvedFrom?: BaseBranchResolution | null, stats?: { __typename?: 'BuildStats', total: number } | null, baseScreenshotBucket?: { __typename?: 'ScreenshotBucket', id: string, commit: string, branch?: string | null } | null, baseBuild?: { __typename?: 'Build', id: string, number: number } | null, pullRequest?: { __typename?: 'GithubPullRequest', id: string, url: string, number: number } | null, metadata?: { __typename?: 'BuildMetadata', testReport?: { __typename?: 'TestReport', status: TestReportStatus } | null } | null } & { ' $fragmentName'?: 'BuildInfos_BuildFragment' }; export type BuildOrphanDialog_BuildFragment = { __typename?: 'Build', baseBranch?: string | null, mode: BuildMode, type?: BuildType | null } & { ' $fragmentName'?: 'BuildOrphanDialog_BuildFragment' }; @@ -2146,7 +2146,7 @@ export type BuildReviewDialog_ProjectFragment = ( ) & { ' $fragmentName'?: 'BuildReviewDialog_ProjectFragment' }; export type BuildSidebar_BuildFragment = ( - { __typename?: 'Build', stats: { __typename?: 'BuildStats', total: number } } + { __typename?: 'Build', stats?: { __typename?: 'BuildStats', total: number } | null } & { ' $fragmentRefs'?: { 'BuildInfos_BuildFragment': BuildInfos_BuildFragment } } ) & { ' $fragmentName'?: 'BuildSidebar_BuildFragment' }; @@ -2219,10 +2219,10 @@ export type ProjectBuilds_Project_BuildsQueryVariables = Exact<{ export type ProjectBuilds_Project_BuildsQuery = { __typename?: 'Query', project?: { __typename?: 'Project', id: string, builds: { __typename?: 'BuildConnection', pageInfo: { __typename?: 'PageInfo', totalCount: number, hasNextPage: boolean }, edges: Array<( - { __typename?: 'Build', id: string, number: number, createdAt: any, name: string, branch?: string | null, commit: string, mode: BuildMode, stats: ( + { __typename?: 'Build', id: string, number: number, createdAt: any, name: string, branch?: string | null, commit: string, mode: BuildMode, stats?: ( { __typename?: 'BuildStats' } & { ' $fragmentRefs'?: { 'BuildStatsIndicator_BuildStatsFragment': BuildStatsIndicator_BuildStatsFragment } } - ), pullRequest?: ( + ) | null, pullRequest?: ( { __typename?: 'GithubPullRequest', id: string } & { ' $fragmentRefs'?: { 'PullRequestButton_PullRequestFragment': PullRequestButton_PullRequestFragment } } ) | null } diff --git a/apps/frontend/src/pages/Build/BuildDetail.tsx b/apps/frontend/src/pages/Build/BuildDetail.tsx index e566f2715..c637dc3bd 100644 --- a/apps/frontend/src/pages/Build/BuildDetail.tsx +++ b/apps/frontend/src/pages/Build/BuildDetail.tsx @@ -746,7 +746,7 @@ export function BuildDetail(props: { - ) : build.stats.total === 0 ? ( + ) : build.stats?.total === 0 ? (
diff --git a/apps/frontend/src/pages/Build/BuildDiffState.tsx b/apps/frontend/src/pages/Build/BuildDiffState.tsx index 3b5566900..b453af74c 100644 --- a/apps/frontend/src/pages/Build/BuildDiffState.tsx +++ b/apps/frontend/src/pages/Build/BuildDiffState.tsx @@ -133,7 +133,7 @@ function createDiffs(count: number): null[] { return Array.from({ length: count }, () => null); } -function getGroupsFromStats(stats: BuildStats): DiffGroup[] { +function getGroupsFromStats(stats: NonNullable): DiffGroup[] { return GROUPS.map((group) => ({ name: group, diffs: createDiffs(stats[group]), diff --git a/apps/frontend/src/pages/Build/BuildInfos.tsx b/apps/frontend/src/pages/Build/BuildInfos.tsx index ba093d32b..9a367f7c8 100644 --- a/apps/frontend/src/pages/Build/BuildInfos.tsx +++ b/apps/frontend/src/pages/Build/BuildInfos.tsx @@ -187,7 +187,7 @@ export function BuildInfos(props: {
Total screenshots count
-
{build ? build.stats.total : "-"}
+
{build.stats ? build.stats.total : "-"}
{build.pullRequest ? ( <> diff --git a/apps/frontend/src/pages/Build/BuildSidebar.tsx b/apps/frontend/src/pages/Build/BuildSidebar.tsx index 072ddf861..62fa07396 100644 --- a/apps/frontend/src/pages/Build/BuildSidebar.tsx +++ b/apps/frontend/src/pages/Build/BuildSidebar.tsx @@ -97,7 +97,7 @@ export const BuildSidebar = memo(function BuildSidebar(props: { }); return (
diff --git a/apps/frontend/src/pages/Project/Builds.tsx b/apps/frontend/src/pages/Project/Builds.tsx index e132ad833..57d73f1c4 100644 --- a/apps/frontend/src/pages/Project/Builds.tsx +++ b/apps/frontend/src/pages/Project/Builds.tsx @@ -150,10 +150,12 @@ const BuildRow = memo(function BuildRow({
- + {build.stats ? ( + + ) : null}