Skip to content

Commit 382f751

Browse files
committed
feat(core): allow to com
1 parent fa5a1d1 commit 382f751

8 files changed

Lines changed: 302 additions & 63 deletions

File tree

‎packages/api-client/src/schema.ts‎

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ export interface components {
282282
GitBranch: string;
283283
/** @description SHA256 hash */
284284
Sha256Hash: string;
285+
/** @description Screenshot file to upload */
286+
ScreenshotUploadRequest: {
287+
key: components["schemas"]["Sha256Hash"];
288+
/** @description Content type of the snapshot file */
289+
contentType: string;
290+
};
285291
/**
286292
* @description A unique identifier for the build
287293
* @example 12345
@@ -390,8 +396,8 @@ export interface components {
390396
} | null;
391397
pwTraceKey?: string | null;
392398
threshold?: number | null;
393-
/** @default image/png */
394-
contentType: string;
399+
/** @description Content type of the snapshot file */
400+
contentType?: string;
395401
};
396402
/** @description Build metadata */
397403
BuildMetadata: {
@@ -479,6 +485,8 @@ export interface components {
479485
/** @enum {string} */
480486
state: "pending" | "running" | "success" | "failed" | "canceled";
481487
};
488+
/** Format: uri */
489+
url: string;
482490
} | null;
483491
};
484492
/** @description Git reference */
@@ -789,8 +797,6 @@ export interface operations {
789797
commit: components["schemas"]["Sha1Hash"];
790798
/** @description The branch the build is running on */
791799
branch: components["schemas"]["GitBranch"];
792-
/** @description Keys of screenshot files */
793-
screenshotKeys: components["schemas"]["Sha256Hash"][];
794800
/** @description Keys of Playwright trace files */
795801
pwTraceKeys?: components["schemas"]["Sha256Hash"][];
796802
/** @description The name of the build (for multi-build setups) */
@@ -830,7 +836,18 @@ export interface operations {
830836
* This is useful when a build is created from an incomplete test suite where some tests are skipped.
831837
*/
832838
subset?: boolean | null;
833-
};
839+
} & ({
840+
/**
841+
* @deprecated
842+
* @description Keys of screenshot files
843+
*/
844+
screenshotKeys: components["schemas"]["Sha256Hash"][];
845+
screenshots?: unknown;
846+
} | {
847+
/** @description Screenshot files to upload */
848+
screenshots: components["schemas"]["ScreenshotUploadRequest"][];
849+
screenshotKeys?: unknown;
850+
});
834851
};
835852
};
836853
responses: {
@@ -842,16 +859,38 @@ export interface operations {
842859
content: {
843860
"application/json": {
844861
build: components["schemas"]["Build"];
845-
screenshots: {
862+
screenshots: ({
846863
key: string;
847-
/** Format: uri */
864+
/**
865+
* Format: uri
866+
* @deprecated
867+
* @description Deprecated. Use postUrl and fields instead.
868+
*/
848869
putUrl: string;
849-
}[];
850-
pwTraces: {
870+
} | {
851871
key: string;
852872
/** Format: uri */
873+
postUrl: string;
874+
fields: {
875+
[key: string]: string;
876+
};
877+
})[];
878+
pwTraces: ({
879+
key: string;
880+
/**
881+
* Format: uri
882+
* @deprecated
883+
* @description Deprecated. Use postUrl and fields instead.
884+
*/
853885
putUrl: string;
854-
}[];
886+
} | {
887+
key: string;
888+
/** Format: uri */
889+
postUrl: string;
890+
fields: {
891+
[key: string]: string;
892+
};
893+
})[];
855894
};
856895
};
857896
};
@@ -1446,6 +1485,15 @@ export interface operations {
14461485
"application/json": components["schemas"]["Error"];
14471486
};
14481487
};
1488+
/** @description Not found */
1489+
404: {
1490+
headers: {
1491+
[name: string]: unknown;
1492+
};
1493+
content: {
1494+
"application/json": components["schemas"]["Error"];
1495+
};
1496+
};
14491497
/** @description Server error */
14501498
500: {
14511499
headers: {
@@ -1455,6 +1503,15 @@ export interface operations {
14551503
"application/json": components["schemas"]["Error"];
14561504
};
14571505
};
1506+
/** @description Service unavailable */
1507+
503: {
1508+
headers: {
1509+
[name: string]: unknown;
1510+
};
1511+
content: {
1512+
"application/json": components["schemas"]["Error"];
1513+
};
1514+
};
14581515
};
14591516
};
14601517
getAuthProject: {
@@ -1880,10 +1937,18 @@ export interface operations {
18801937
content: {
18811938
"application/json": {
18821939
/**
1883-
* @description Overall review conclusion for the build: "APPROVE" or "REQUEST_CHANGES"
1940+
* @description Review event to apply to the build: "APPROVE", "REJECT" or "COMMENT". Required when `conclusion` is not provided.
1941+
* @enum {string}
1942+
*/
1943+
event?: "APPROVE" | "REJECT" | "COMMENT";
1944+
/**
1945+
* @deprecated
1946+
* @description Deprecated: use `event` instead. Overall review conclusion for the build: "APPROVE" or "REQUEST_CHANGES".
18841947
* @enum {string}
18851948
*/
1886-
conclusion: "APPROVE" | "REQUEST_CHANGES";
1949+
conclusion?: "APPROVE" | "REQUEST_CHANGES";
1950+
/** @description Optional comment to attach to the review. Expected as the JSON representation of a rich-text document. */
1951+
body?: unknown;
18871952
/**
18881953
* @description Optional per-snapshot review decisions. When omitted, only the build-level review is recorded.
18891954
* @default []
@@ -1910,10 +1975,19 @@ export interface operations {
19101975
"application/json": {
19111976
id: string;
19121977
/** @enum {string} */
1913-
state: "approved" | "rejected";
1978+
state: "approved" | "rejected" | "commented" | "pending";
19141979
};
19151980
};
19161981
};
1982+
/** @description Invalid parameters */
1983+
400: {
1984+
headers: {
1985+
[name: string]: unknown;
1986+
};
1987+
content: {
1988+
"application/json": components["schemas"]["Error"];
1989+
};
1990+
};
19171991
/** @description Unauthorized */
19181992
401: {
19191993
headers: {

‎packages/core/mocks/handlers/createBuild.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { http, HttpResponse } from "msw";
33
type CreateBuildParams = never;
44
type CreateBuildRequestBody = {
55
commit: string;
6-
screenshotKeys: string[];
6+
screenshots: { key: string; contentType: string }[];
7+
pwTraceKeys?: string[];
78
branch?: string | null;
89
name?: string | null;
910
parallel?: boolean | null;
@@ -18,7 +19,13 @@ type CreateBuildResponseBody = {
1819
};
1920
screenshots: {
2021
key: string;
21-
putUrl: string;
22+
postUrl: string;
23+
fields: Record<string, string>;
24+
}[];
25+
pwTraces: {
26+
key: string;
27+
postUrl: string;
28+
fields: Record<string, string>;
2229
}[];
2330
};
2431

@@ -27,15 +34,27 @@ export const createBuild = http.post<
2734
CreateBuildRequestBody,
2835
CreateBuildResponseBody
2936
>("https://api.argos-ci.dev/builds", async ({ request }) => {
30-
const { screenshotKeys } = await request.json();
37+
const body = await request.json();
3138
return HttpResponse.json({
3239
build: {
3340
id: "123",
3441
url: "https://app.argos-ci.dev/builds/123",
3542
},
36-
screenshots: screenshotKeys.map((key) => ({
43+
screenshots: body.screenshots.map((screenshot) => ({
44+
key: screenshot.key,
45+
postUrl: `https://api.s3.dev/upload/${screenshot.key}`,
46+
fields: {
47+
key: screenshot.key,
48+
"Content-Type": screenshot.contentType,
49+
},
50+
})),
51+
pwTraces: (body.pwTraceKeys ?? []).map((key) => ({
3752
key,
38-
putUrl: `https://api.s3.dev/upload/${key}`,
53+
postUrl: `https://api.s3.dev/upload/${key}`,
54+
fields: {
55+
key,
56+
"Content-Type": "application/zip",
57+
},
3958
})),
4059
});
4160
});
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { http } from "msw";
22

3-
export const uploadScreenshot = http.put(
3+
export const uploadScreenshot = http.post(
44
"https://api.s3.dev/upload/*",
5-
async () => {
6-
return new Response(null, { status: 201 });
5+
async ({ request }) => {
6+
const formData = await request.formData();
7+
if (!formData.has("file")) {
8+
return new Response(null, { status: 400 });
9+
}
10+
return new Response(null, { status: 204 });
711
},
812
);

‎packages/core/src/s3.test.ts‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
import { describe, it } from "vitest";
33
import { join } from "node:path";
44
// import { fileURLToPath } from "node:url";
5-
import { setupMockServer } from "../mocks/server";
6-
import { uploadFile } from "./s3";
5+
import { server, setupMockServer } from "../mocks/server";
6+
import { uploadFile, uploadFileWithPresignedPost } from "./s3";
7+
import { http } from "msw";
78

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

1011
setupMockServer();
1112

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

2123
it("uploads big images", async () => {
24+
mockPutUpload();
2225
await uploadFile({
2326
path: join(__dirname, "../../../__fixtures__/png-10mb.png"),
2427
url: "https://api.s3.dev/upload/123",
2528
contentType: "image/png",
2629
});
2730
});
31+
32+
it("uploads with presigned POST fields", async () => {
33+
await uploadFileWithPresignedPost({
34+
path: join(__dirname, "../../../__fixtures__/screenshots/penelope.png"),
35+
url: "https://api.s3.dev/upload/123",
36+
contentType: "image/png",
37+
fields: {
38+
key: "123",
39+
"Content-Type": "image/png",
40+
},
41+
});
42+
});
2843
});
44+
45+
function mockPutUpload() {
46+
server.use(
47+
http.put("https://api.s3.dev/upload/*", async () => {
48+
return new Response(null, { status: 201 });
49+
}),
50+
);
51+
}

‎packages/core/src/s3.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { readFile } from "node:fs/promises";
2+
import { basename } from "node:path";
23

34
interface UploadInput {
45
url: string;
56
path: string;
67
contentType: string;
78
}
89

10+
interface PresignedPostUploadInput extends UploadInput {
11+
fields: Record<string, string>;
12+
}
13+
914
export async function uploadFile(input: UploadInput): Promise<void> {
1015
const file = await readFile(input.path);
1116
const response = await fetch(input.url, {
@@ -22,3 +27,29 @@ export async function uploadFile(input: UploadInput): Promise<void> {
2227
);
2328
}
2429
}
30+
31+
export async function uploadFileWithPresignedPost(
32+
input: PresignedPostUploadInput,
33+
): Promise<void> {
34+
const file = await readFile(input.path);
35+
const formData = new FormData();
36+
for (const [key, value] of Object.entries(input.fields)) {
37+
formData.append(key, value);
38+
}
39+
formData.append(
40+
"file",
41+
new Blob([new Uint8Array(file)], { type: input.contentType }),
42+
basename(input.path),
43+
);
44+
45+
const response = await fetch(input.url, {
46+
method: "POST",
47+
signal: AbortSignal.timeout(30_000),
48+
body: formData,
49+
});
50+
if (!response.ok) {
51+
throw new Error(
52+
`Failed to upload file to ${input.url}: ${response.status} ${response.statusText}`,
53+
);
54+
}
55+
}

‎packages/core/src/skip.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function skip(
4949
runId: config.runId,
5050
runAttempt: config.runAttempt,
5151
skipped: true,
52-
screenshotKeys: [],
52+
screenshots: [],
5353
pwTraceKeys: [],
5454
parentCommits: [],
5555
},

0 commit comments

Comments
 (0)