Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 87 additions & 13 deletions packages/api-client/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ export interface components {
GitBranch: string;
/** @description SHA256 hash */
Sha256Hash: string;
/** @description Screenshot file to upload */
ScreenshotUploadRequest: {
key: components["schemas"]["Sha256Hash"];
/** @description Content type of the snapshot file */
contentType: string;
};
/**
* @description A unique identifier for the build
* @example 12345
Expand Down Expand Up @@ -390,8 +396,8 @@ export interface components {
} | null;
pwTraceKey?: string | null;
threshold?: number | null;
/** @default image/png */
contentType: string;
/** @description Content type of the snapshot file */
contentType?: string;
};
/** @description Build metadata */
BuildMetadata: {
Expand Down Expand Up @@ -479,6 +485,8 @@ export interface components {
/** @enum {string} */
state: "pending" | "running" | "success" | "failed" | "canceled";
};
/** Format: uri */
url: string;
} | null;
};
/** @description Git reference */
Expand Down Expand Up @@ -789,8 +797,6 @@ export interface operations {
commit: components["schemas"]["Sha1Hash"];
/** @description The branch the build is running on */
branch: components["schemas"]["GitBranch"];
/** @description Keys of screenshot files */
screenshotKeys: components["schemas"]["Sha256Hash"][];
/** @description Keys of Playwright trace files */
pwTraceKeys?: components["schemas"]["Sha256Hash"][];
/** @description The name of the build (for multi-build setups) */
Expand Down Expand Up @@ -830,7 +836,18 @@ export interface operations {
* This is useful when a build is created from an incomplete test suite where some tests are skipped.
*/
subset?: boolean | null;
};
} & ({
/**
* @deprecated
* @description Keys of screenshot files
*/
screenshotKeys: components["schemas"]["Sha256Hash"][];
screenshots?: unknown;
} | {
/** @description Screenshot files to upload */
screenshots: components["schemas"]["ScreenshotUploadRequest"][];
screenshotKeys?: unknown;
Comment thread
gregberge marked this conversation as resolved.
});
};
};
responses: {
Expand All @@ -842,16 +859,38 @@ export interface operations {
content: {
"application/json": {
build: components["schemas"]["Build"];
screenshots: {
screenshots: ({
key: string;
/** Format: uri */
/**
* Format: uri
* @deprecated
* @description Deprecated. Use postUrl and fields instead.
*/
putUrl: string;
}[];
pwTraces: {
} | {
key: string;
/** Format: uri */
postUrl: string;
fields: {
[key: string]: string;
};
})[];
pwTraces: ({
key: string;
/**
* Format: uri
* @deprecated
* @description Deprecated. Use postUrl and fields instead.
*/
putUrl: string;
}[];
} | {
key: string;
/** Format: uri */
postUrl: string;
fields: {
[key: string]: string;
};
})[];
};
};
};
Expand Down Expand Up @@ -1446,6 +1485,15 @@ export interface operations {
"application/json": components["schemas"]["Error"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Server error */
500: {
headers: {
Expand All @@ -1455,6 +1503,15 @@ export interface operations {
"application/json": components["schemas"]["Error"];
};
};
/** @description Service unavailable */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
};
getAuthProject: {
Expand Down Expand Up @@ -1880,10 +1937,18 @@ export interface operations {
content: {
"application/json": {
/**
* @description Overall review conclusion for the build: "APPROVE" or "REQUEST_CHANGES"
* @description Review event to apply to the build: "APPROVE", "REJECT" or "COMMENT". Required when `conclusion` is not provided.
* @enum {string}
*/
event?: "APPROVE" | "REJECT" | "COMMENT";
/**
* @deprecated
* @description Deprecated: use `event` instead. Overall review conclusion for the build: "APPROVE" or "REQUEST_CHANGES".
* @enum {string}
*/
conclusion: "APPROVE" | "REQUEST_CHANGES";
conclusion?: "APPROVE" | "REQUEST_CHANGES";
/** @description Optional comment to attach to the review. Expected as the JSON representation of a rich-text document. */
body?: unknown;
/**
* @description Optional per-snapshot review decisions. When omitted, only the build-level review is recorded.
* @default []
Expand All @@ -1910,10 +1975,19 @@ export interface operations {
"application/json": {
id: string;
/** @enum {string} */
state: "approved" | "rejected";
state: "approved" | "rejected" | "commented" | "pending";
};
};
};
/** @description Invalid parameters */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description Unauthorized */
401: {
headers: {
Expand Down
29 changes: 24 additions & 5 deletions packages/core/mocks/handlers/createBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { http, HttpResponse } from "msw";
type CreateBuildParams = never;
type CreateBuildRequestBody = {
commit: string;
screenshotKeys: string[];
screenshots: { key: string; contentType: string }[];
pwTraceKeys?: string[];
branch?: string | null;
name?: string | null;
parallel?: boolean | null;
Expand All @@ -18,7 +19,13 @@ type CreateBuildResponseBody = {
};
screenshots: {
key: string;
putUrl: string;
postUrl: string;
fields: Record<string, string>;
}[];
pwTraces: {
key: string;
postUrl: string;
fields: Record<string, string>;
}[];
};

Expand All @@ -27,15 +34,27 @@ export const createBuild = http.post<
CreateBuildRequestBody,
CreateBuildResponseBody
>("https://api.argos-ci.dev/builds", async ({ request }) => {
const { screenshotKeys } = await request.json();
const body = await request.json();
return HttpResponse.json({
build: {
id: "123",
url: "https://app.argos-ci.dev/builds/123",
},
screenshots: screenshotKeys.map((key) => ({
screenshots: body.screenshots.map((screenshot) => ({
key: screenshot.key,
postUrl: `https://api.s3.dev/upload/${screenshot.key}`,
fields: {
key: screenshot.key,
"Content-Type": screenshot.contentType,
},
})),
pwTraces: (body.pwTraceKeys ?? []).map((key) => ({
key,
putUrl: `https://api.s3.dev/upload/${key}`,
postUrl: `https://api.s3.dev/upload/${key}`,
fields: {
key,
"Content-Type": "application/zip",
},
})),
});
});
10 changes: 7 additions & 3 deletions packages/core/mocks/handlers/uploadScreenshot.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { http } from "msw";

export const uploadScreenshot = http.put(
export const uploadScreenshot = http.post(
"https://api.s3.dev/upload/*",
async () => {
return new Response(null, { status: 201 });
async ({ request }) => {
const formData = await request.formData();
if (!formData.has("file")) {
return new Response(null, { status: 400 });
}
return new Response(null, { status: 204 });
},
);
27 changes: 25 additions & 2 deletions packages/core/src/s3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
import { describe, it } from "vitest";
import { join } from "node:path";
// import { fileURLToPath } from "node:url";
import { setupMockServer } from "../mocks/server";
import { uploadFile } from "./s3";
import { server, setupMockServer } from "../mocks/server";
import { uploadFile, uploadFileWithPresignedPost } from "./s3";
import { http } from "msw";

// const __dirname = fileURLToPath(new URL(".", import.meta.url));

setupMockServer();

describe("#upload", () => {
it("uploads", async () => {
mockPutUpload();
await uploadFile({
path: join(__dirname, "../../../__fixtures__/screenshots/penelope.png"),
url: "https://api.s3.dev/upload/123",
Expand All @@ -19,10 +21,31 @@ describe("#upload", () => {
});

it("uploads big images", async () => {
mockPutUpload();
await uploadFile({
path: join(__dirname, "../../../__fixtures__/png-10mb.png"),
url: "https://api.s3.dev/upload/123",
contentType: "image/png",
});
});

it("uploads with presigned POST fields", async () => {
await uploadFileWithPresignedPost({
path: join(__dirname, "../../../__fixtures__/screenshots/penelope.png"),
url: "https://api.s3.dev/upload/123",
contentType: "image/png",
fields: {
key: "123",
"Content-Type": "image/png",
},
});
});
});

function mockPutUpload() {
server.use(
http.put("https://api.s3.dev/upload/*", async () => {
return new Response(null, { status: 201 });
}),
);
}
31 changes: 31 additions & 0 deletions packages/core/src/s3.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

interface UploadInput {
url: string;
path: string;
contentType: string;
}

interface PresignedPostUploadInput extends UploadInput {
fields: Record<string, string>;
}

export async function uploadFile(input: UploadInput): Promise<void> {
const file = await readFile(input.path);
const response = await fetch(input.url, {
Expand All @@ -22,3 +27,29 @@ export async function uploadFile(input: UploadInput): Promise<void> {
);
}
}

export async function uploadFileWithPresignedPost(
input: PresignedPostUploadInput,
): Promise<void> {
const file = await readFile(input.path);
const formData = new FormData();
for (const [key, value] of Object.entries(input.fields)) {
formData.append(key, value);
}
formData.append(
"file",
new Blob([new Uint8Array(file)], { type: input.contentType }),
basename(input.path),
);

const response = await fetch(input.url, {
method: "POST",
signal: AbortSignal.timeout(30_000),
body: formData,
});
if (!response.ok) {
throw new Error(
`Failed to upload file to ${input.url}: ${response.status} ${response.statusText}`,
);
}
}
2 changes: 1 addition & 1 deletion packages/core/src/skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function skip(
runId: config.runId,
runAttempt: config.runAttempt,
skipped: true,
screenshotKeys: [],
screenshots: [],
pwTraceKeys: [],
parentCommits: [],
},
Expand Down
Loading
Loading