diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 1022f1e..4d937fb 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -1,6 +1,8 @@ import createFetchClient, { FetchResponse } from "openapi-fetch"; import type { paths } from "./schema"; +export * as ArgosAPISchema from "./schema"; + export type ArgosAPIClient = ReturnType; /** diff --git a/packages/api-client/src/schema.ts b/packages/api-client/src/schema.ts index f51681e..bae7d97 100644 --- a/packages/api-client/src/schema.ts +++ b/packages/api-client/src/schema.ts @@ -88,17 +88,23 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { + /** @description SHA1 hash */ + Sha1Hash: string; + /** @description SHA256 hash */ + Sha256Hash: string; /** @description Build */ Build: { - /** - * @description A unique identifier for the build - * @example 12345 - */ - id: string; + id: components["schemas"]["BuildId"]; + /** @description The build number */ number: number; + /** @description The status of the build */ status: ("accepted" | "rejected") | ("stable" | "diffDetected") | ("expired" | "pending" | "progress" | "error" | "aborted"); - /** Format: uri */ + /** + * Format: uri + * @description The URL of the build + */ url: string; + /** @description The notification payload for the build */ notification: { description: string; context: string; @@ -112,6 +118,11 @@ export interface components { }; } | null; }; + /** + * @description A unique identifier for the build + * @example 12345 + */ + BuildId: string; /** @description Error response */ Error: { error: string; @@ -124,6 +135,76 @@ export interface components { * @example 12345 */ buildId: string; + /** @description Screenshot input */ + ScreenshotInput: { + key: string; + name: string; + baseName?: string | null; + metadata?: { + url?: string; + viewport?: { + width: number; + height: number; + }; + /** @enum {string} */ + colorScheme?: "light" | "dark"; + /** @enum {string} */ + mediaType?: "screen" | "print"; + test?: { + id?: string; + title: string; + titlePath: string[]; + retries?: number; + retry?: number; + repeat?: number; + location?: { + file: string; + line: number; + column: number; + }; + } | null; + browser?: { + name: string; + version: string; + }; + automationLibrary: { + name: string; + version: string; + }; + sdk: { + name: string; + version: string; + }; + } | null; + pwTraceKey?: string | null; + threshold?: number | null; + }; + /** @description Build metadata */ + BuildMetadata: { + testReport?: { + /** @enum {string} */ + status: "passed" | "failed" | "timedout" | "interrupted"; + stats?: { + startTime?: string; + duration?: number; + }; + }; + }; + /** @description Project */ + Project: { + id: string; + defaultBaseBranch: string; + hasRemoteContentAccess: boolean; + }; + /** @description Page information */ + PageInfo: { + /** @description Total number of items */ + total: number; + /** @description Current page number */ + page: number; + /** @description Number of items per page */ + perPage: number; + }; }; responses: never; parameters: never; @@ -143,10 +224,10 @@ export interface operations { requestBody?: { content: { "application/json": { - commit: string; + commit: components["schemas"]["Sha1Hash"]; branch: string; - screenshotKeys: string[]; - pwTraceKeys?: string[]; + screenshotKeys: components["schemas"]["Sha256Hash"][]; + pwTraceKeys?: components["schemas"]["Sha256Hash"][]; name?: string | null; parallel?: boolean | null; parallelNonce?: string | null; @@ -318,52 +399,11 @@ export interface operations { requestBody?: { content: { "application/json": { - screenshots: { - key: string; - name: string; - baseName?: string | null; - metadata?: { - url?: string; - viewport?: { - width: number; - height: number; - }; - /** @enum {string} */ - colorScheme?: "light" | "dark"; - /** @enum {string} */ - mediaType?: "screen" | "print"; - test?: { - id?: string; - title: string; - titlePath: string[]; - retries?: number; - retry?: number; - repeat?: number; - location?: { - file: string; - line: number; - column: number; - }; - } | null; - browser?: { - name: string; - version: string; - }; - automationLibrary: { - name: string; - version: string; - }; - sdk: { - name: string; - version: string; - }; - } | null; - pwTraceKey?: string | null; - threshold?: number | null; - }[]; + screenshots: components["schemas"]["ScreenshotInput"][]; parallel?: boolean | null; parallelTotal?: number | null; parallelIndex?: number | null; + metadata?: components["schemas"]["BuildMetadata"]; }; }; }; @@ -450,11 +490,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": { - id: string; - defaultBaseBranch: string; - hasRemoteContentAccess: boolean; - }; + "application/json": components["schemas"]["Project"]; }; }; /** @description Unauthorized */ @@ -484,8 +520,7 @@ export interface operations { perPage?: string; /** @description Page number */ page?: string; - /** @description Commit hash. */ - commit?: string; + commit?: components["schemas"]["Sha1Hash"]; /** @description Only return the latest builds created, unique by name and commit. */ distinctName?: string; }; @@ -502,11 +537,7 @@ export interface operations { }; content: { "application/json": { - pageInfo: { - total: number; - page: number; - perPage: number; - }; + pageInfo: components["schemas"]["PageInfo"]; results: components["schemas"]["Build"][]; }; }; diff --git a/packages/core/src/upload.ts b/packages/core/src/upload.ts index ef75ed2..cc93d05 100644 --- a/packages/core/src/upload.ts +++ b/packages/core/src/upload.ts @@ -1,4 +1,8 @@ -import { createClient, throwAPIError } from "@argos-ci/api-client"; +import { + ArgosAPISchema, + createClient, + throwAPIError, +} from "@argos-ci/api-client"; import { readConfig } from "./config"; import { discoverScreenshots } from "./discovery"; import { optimizeScreenshot } from "./optimize"; @@ -16,6 +20,8 @@ import { getMergeBaseCommitSha } from "./ci-environment"; */ const CHUNK_SIZE = 10; +type BuildMetadata = ArgosAPISchema.components["schemas"]["BuildMetadata"]; + export interface UploadParameters { /** * Globs matching image file paths to upload @@ -88,6 +94,10 @@ export interface UploadParameters { * @default 0.5 */ threshold?: number; + /** + * Build metadata. + */ + metadata?: BuildMetadata; } async function getConfigFromOptions({ @@ -324,6 +334,7 @@ export async function upload(params: UploadParameters) { parallel: config.parallel, parallelTotal: config.parallelTotal, parallelIndex: config.parallelIndex, + metadata: params.metadata, }, }); diff --git a/packages/cypress/src/task.ts b/packages/cypress/src/task.ts index b6e6e1e..ea640f7 100644 --- a/packages/cypress/src/task.ts +++ b/packages/cypress/src/task.ts @@ -14,6 +14,14 @@ export type RegisterArgosTaskOptions = Omit< uploadToArgos?: boolean; }; +function checkIsCypressFailedResult( + results: + | CypressCommandLine.CypressFailedRunResult + | CypressCommandLine.CypressRunResult, +): results is CypressCommandLine.CypressFailedRunResult { + return "status" in results && results.status === "failed"; +} + export function registerArgosTask( on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions, @@ -37,16 +45,33 @@ export function registerArgosTask( return { path: newPath }; }); - on("after:run", async () => { + on("after:run", async (results) => { const { screenshotsFolder } = config; - if (!screenshotsFolder) return; + if (!screenshotsFolder) { + return; + } const { uploadToArgos = true } = options || {}; - if (!uploadToArgos) return; + if (!uploadToArgos) { + return; + } + const res = await upload({ + ...options, files: ["**/*.png"], root: screenshotsFolder, - ...options, + metadata: { + testReport: checkIsCypressFailedResult(results) + ? { status: "failed" } + : { + status: "passed", + stats: { + startTime: results.startedTestsAt, + duration: results.totalDuration, + }, + }, + }, }); + console.log(`✅ Argos build created: ${res.build.url}`); }); } diff --git a/packages/playwright/src/reporter.ts b/packages/playwright/src/reporter.ts index dc9be2c..59295f8 100644 --- a/packages/playwright/src/reporter.ts +++ b/packages/playwright/src/reporter.ts @@ -234,7 +234,7 @@ class ArgosReporter implements Reporter { ); } - async onEnd(_result: FullResult) { + async onEnd(result: FullResult) { debug("ArgosReporter:onEnd"); const rootUploadDir = await this.getRootUploadDirectory(); if (!this.uploadToArgos) { @@ -258,7 +258,17 @@ class ArgosReporter implements Reporter { files: ["**/*.png"], parallel: parallel ?? undefined, ...this.config, - }; + buildName: undefined, // We will set it later + metadata: { + testReport: { + status: result.status, + stats: { + startTime: result.startTime.toISOString(), + duration: result.duration, + }, + }, + }, + } satisfies Partial; try { if (checkIsDynamicBuildName(buildNameConfig)) { debug(