@empiricalrun/playwright-utils 0.64.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/api-worker-client.d.ts +3 -0
- package/dist/api-worker-client.d.ts.map +1 -1
- package/dist/api-worker-client.js +6 -0
- package/dist/generated/api-worker-openapi.d.ts +321 -532
- package/dist/generated/api-worker-openapi.d.ts.map +1 -1
- package/dist/reporter/presigned-upload.d.ts +15 -0
- package/dist/reporter/presigned-upload.d.ts.map +1 -0
- package/dist/reporter/presigned-upload.js +96 -0
- package/dist/reporter/uploader.d.ts +10 -1
- package/dist/reporter/uploader.d.ts.map +1 -1
- package/dist/reporter/uploader.js +51 -2
- package/package.json +3 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FileMap } from "@empiricalrun/r2-uploader";
|
|
2
|
+
import { ApiWorkerClient } from "../api-worker-client";
|
|
3
|
+
export type PresignedUploadFile = {
|
|
4
|
+
filePath: string;
|
|
5
|
+
reportPath: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function directoryUploadFiles(sourceDir: string, reportDir: string): PresignedUploadFile[];
|
|
8
|
+
export declare function uploadPresigned({ client, runId, baseUrl, destinationDir, files, }: {
|
|
9
|
+
client: ApiWorkerClient;
|
|
10
|
+
runId: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
destinationDir: string;
|
|
13
|
+
files: PresignedUploadFile[];
|
|
14
|
+
}): Promise<FileMap>;
|
|
15
|
+
//# sourceMappingURL=presigned-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presigned-upload.d.ts","sourceRoot":"","sources":["../../src/reporter/presigned-upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAkB,MAAM,2BAA2B,CAAC;AAOzE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAKvD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AASF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,mBAAmB,EAAE,CAUvB;AA8CD,wBAAsB,eAAe,CAAC,EACpC,MAAM,EACN,KAAK,EACL,OAAO,EACP,cAAc,EACd,KAAK,GACN,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B,GAAG,OAAO,CAAC,OAAO,CAAC,CAwCnB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.directoryUploadFiles = directoryUploadFiles;
|
|
7
|
+
exports.uploadPresigned = uploadPresigned;
|
|
8
|
+
const r2_uploader_1 = require("@empiricalrun/r2-uploader");
|
|
9
|
+
const async_retry_1 = __importDefault(require("async-retry"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const mime_1 = __importDefault(require("mime"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const stream_1 = require("stream");
|
|
14
|
+
const logger_1 = require("../logger");
|
|
15
|
+
const MAX_FILES_PER_REQUEST = 100;
|
|
16
|
+
function listFiles(dir) {
|
|
17
|
+
return fs_1.default.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
18
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
19
|
+
return entry.isDirectory() ? listFiles(fullPath) : [fullPath];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function directoryUploadFiles(sourceDir, reportDir) {
|
|
23
|
+
return listFiles(sourceDir)
|
|
24
|
+
.filter((filePath) => !filePath.includes(".gitkeep"))
|
|
25
|
+
.map((filePath) => ({
|
|
26
|
+
filePath,
|
|
27
|
+
reportPath: path_1.default.posix.join(reportDir, path_1.default.relative(sourceDir, filePath).split(path_1.default.sep).join("/")),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
async function putFile(uploadUrl, filePath, size, headers) {
|
|
31
|
+
await (0, async_retry_1.default)(async (bail) => {
|
|
32
|
+
const response = await fetch(uploadUrl, {
|
|
33
|
+
method: "PUT",
|
|
34
|
+
headers: { ...headers, "Content-Length": String(size) },
|
|
35
|
+
body: stream_1.Readable.toWeb(fs_1.default.createReadStream(filePath)),
|
|
36
|
+
duplex: "half",
|
|
37
|
+
});
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const detail = (await response.text().catch(() => "")).slice(0, 500);
|
|
42
|
+
const error = new Error(`Report upload of ${filePath} failed: ${response.status} ${detail}`);
|
|
43
|
+
if (response.status < 500 &&
|
|
44
|
+
response.status !== 408 &&
|
|
45
|
+
response.status !== 429) {
|
|
46
|
+
bail(error);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}, {
|
|
51
|
+
retries: 5,
|
|
52
|
+
factor: 3,
|
|
53
|
+
minTimeout: 1_000,
|
|
54
|
+
maxTimeout: 60_000,
|
|
55
|
+
randomize: true,
|
|
56
|
+
onRetry: (_err, attempt) => {
|
|
57
|
+
logger_1.logger.debug(`[Uploader] upload retry ${attempt}: ${filePath}`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function uploadPresigned({ client, runId, baseUrl, destinationDir, files, }) {
|
|
62
|
+
const urls = {};
|
|
63
|
+
for (let i = 0; i < files.length; i += MAX_FILES_PER_REQUEST) {
|
|
64
|
+
const batch = await Promise.all(files.slice(i, i + MAX_FILES_PER_REQUEST).map(async (file) => {
|
|
65
|
+
const size = (await fs_1.default.promises.stat(file.filePath)).size;
|
|
66
|
+
return {
|
|
67
|
+
...file,
|
|
68
|
+
size,
|
|
69
|
+
contentType: mime_1.default.getType(file.filePath) || "application/octet-stream",
|
|
70
|
+
storageClass: (0, r2_uploader_1.iaStorageClass)("r2", size)?.StorageClass,
|
|
71
|
+
};
|
|
72
|
+
}));
|
|
73
|
+
const { data } = await client.createTestReportUploadUrls({
|
|
74
|
+
run_id: runId,
|
|
75
|
+
files: batch.map((file) => ({
|
|
76
|
+
path: file.reportPath,
|
|
77
|
+
content_type: file.contentType,
|
|
78
|
+
storage_class: file.storageClass,
|
|
79
|
+
})),
|
|
80
|
+
});
|
|
81
|
+
await Promise.all(batch.map(async (file, index) => {
|
|
82
|
+
const signed = data.files[index];
|
|
83
|
+
if (!signed || signed.path !== file.reportPath) {
|
|
84
|
+
throw new Error(`No upload URL returned for ${file.reportPath}`);
|
|
85
|
+
}
|
|
86
|
+
await putFile(signed.upload_url, file.filePath, file.size, {
|
|
87
|
+
"Content-Type": file.contentType,
|
|
88
|
+
...(file.storageClass && {
|
|
89
|
+
"x-amz-storage-class": file.storageClass,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
urls[file.filePath] = `${baseUrl}/${destinationDir}/${file.reportPath}`;
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return urls;
|
|
96
|
+
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { StorageCredentials, UploadStorageClass } from "@empiricalrun/r2-uploader";
|
|
2
|
+
import { ApiWorkerClient } from "../api-worker-client";
|
|
3
|
+
type PresignedCredentials = {
|
|
4
|
+
provider: "r2-presigned";
|
|
5
|
+
client: ApiWorkerClient;
|
|
6
|
+
};
|
|
7
|
+
export type UploaderCredentials = StorageCredentials | PresignedCredentials;
|
|
2
8
|
export interface UploaderConfig {
|
|
3
9
|
projectSlug: string;
|
|
4
10
|
runId: string;
|
|
5
11
|
baseUrl: string;
|
|
6
12
|
uploadBucket: string;
|
|
7
|
-
credentials:
|
|
13
|
+
credentials: UploaderCredentials;
|
|
8
14
|
}
|
|
9
15
|
export declare class Uploader {
|
|
10
16
|
private _baseUrl;
|
|
11
17
|
private _uploadBucket;
|
|
18
|
+
private _runId;
|
|
12
19
|
private _destinationDir;
|
|
13
20
|
private _credentials;
|
|
14
21
|
constructor(config: UploaderConfig);
|
|
@@ -19,10 +26,12 @@ export declare class Uploader {
|
|
|
19
26
|
get baseUrl(): string;
|
|
20
27
|
get destinationDir(): string;
|
|
21
28
|
get urlPrefix(): string;
|
|
29
|
+
private _presignedTask;
|
|
22
30
|
uploadFile(filePath: string, destinationPath: string, storageClass?: UploadStorageClass): Promise<string | null>;
|
|
23
31
|
uploadDirectory(sourceDir: string, destinationPrefix: string): Promise<void>;
|
|
24
32
|
waitForUploads(): Promise<void>;
|
|
25
33
|
}
|
|
26
34
|
export declare function getUploader(): Uploader | null;
|
|
27
35
|
export declare function createUploader(): Uploader | null;
|
|
36
|
+
export {};
|
|
28
37
|
//# sourceMappingURL=uploader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"uploader.d.ts","sourceRoot":"","sources":["../../src/reporter/uploader.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,kBAAkB,EAElB,kBAAkB,EAEnB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAWvD,KAAK,oBAAoB,GAAG;IAC1B,QAAQ,EAAE,cAAc,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,YAAY,CAAsB;IAE1C,YAAY,MAAM,EAAE,cAAc,EAUjC;IAED,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAgBhC,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAUhC,MAAM,CAAC,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAKzE;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,cAAc;IAoBhB,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,kBAAkB,GAChC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAyCxB;IAEK,eAAe,CACnB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,IAAI,CAAC,CAyBf;IAEK,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAyBpC;CACF;AAID,wBAAgB,WAAW,IAAI,QAAQ,GAAG,IAAI,CAM7C;AAED,wBAAgB,cAAc,IAAI,QAAQ,GAAG,IAAI,CAmEhD"}
|
|
@@ -8,15 +8,21 @@ exports.getUploader = getUploader;
|
|
|
8
8
|
exports.createUploader = createUploader;
|
|
9
9
|
const r2_uploader_1 = require("@empiricalrun/r2-uploader");
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const api_worker_client_1 = require("../api-worker-client");
|
|
11
12
|
const logger_1 = require("../logger");
|
|
13
|
+
const presigned_upload_1 = require("./presigned-upload");
|
|
14
|
+
const R2_REPORTS_BASE_URL = "https://reports-r2.empirical.run";
|
|
15
|
+
const R2_REPORTS_BUCKET = "test-report";
|
|
12
16
|
class Uploader {
|
|
13
17
|
_baseUrl;
|
|
14
18
|
_uploadBucket;
|
|
19
|
+
_runId;
|
|
15
20
|
_destinationDir;
|
|
16
21
|
_credentials;
|
|
17
22
|
constructor(config) {
|
|
18
23
|
this._baseUrl = config.baseUrl;
|
|
19
24
|
this._uploadBucket = config.uploadBucket;
|
|
25
|
+
this._runId = config.runId;
|
|
20
26
|
this._destinationDir = `${config.projectSlug}/${config.runId}`;
|
|
21
27
|
this._credentials = config.credentials;
|
|
22
28
|
logger_1.logger.debug(`[Uploader] Initialized with destination: ${this._destinationDir}, provider: ${this._credentials.provider}`);
|
|
@@ -61,8 +67,31 @@ class Uploader {
|
|
|
61
67
|
get urlPrefix() {
|
|
62
68
|
return `${this._baseUrl}/${this._destinationDir}`;
|
|
63
69
|
}
|
|
70
|
+
_presignedTask(credentials, files) {
|
|
71
|
+
return async () => {
|
|
72
|
+
try {
|
|
73
|
+
return await (0, presigned_upload_1.uploadPresigned)({
|
|
74
|
+
client: credentials.client,
|
|
75
|
+
runId: this._runId,
|
|
76
|
+
baseUrl: this._baseUrl,
|
|
77
|
+
destinationDir: this._destinationDir,
|
|
78
|
+
files,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
logger_1.logger.error("[Uploader] Error uploading files", error);
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
64
87
|
async uploadFile(filePath, destinationPath, storageClass) {
|
|
65
88
|
try {
|
|
89
|
+
if (this._credentials.provider === "r2-presigned") {
|
|
90
|
+
const result = await (0, r2_uploader_1.sendTaskToQueue)(this._presignedTask(this._credentials, [
|
|
91
|
+
{ filePath, reportPath: destinationPath },
|
|
92
|
+
]));
|
|
93
|
+
return (result && result[filePath]) || null;
|
|
94
|
+
}
|
|
66
95
|
const fullDestinationPath = path_1.default.join(this._destinationDir, destinationPath);
|
|
67
96
|
const sourceDir = path_1.default.dirname(filePath);
|
|
68
97
|
const uploadTask = (0, r2_uploader_1.createUploadTask)({
|
|
@@ -90,6 +119,10 @@ class Uploader {
|
|
|
90
119
|
}
|
|
91
120
|
}
|
|
92
121
|
async uploadDirectory(sourceDir, destinationPrefix) {
|
|
122
|
+
if (this._credentials.provider === "r2-presigned") {
|
|
123
|
+
await (0, r2_uploader_1.sendTaskToQueue)(this._presignedTask(this._credentials, (0, presigned_upload_1.directoryUploadFiles)(sourceDir, destinationPrefix)));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
93
126
|
const fullDestinationDir = path_1.default.join(this._destinationDir, destinationPrefix);
|
|
94
127
|
const uploadTask = (0, r2_uploader_1.createUploadTask)({
|
|
95
128
|
sourceDir,
|
|
@@ -145,6 +178,9 @@ function createUploader() {
|
|
|
145
178
|
logger_1.logger.debug(`[Uploader] REPORT_STORAGE_BACKEND must be "s3" or "r2" (got ${backend ?? "unset"})`);
|
|
146
179
|
return null;
|
|
147
180
|
}
|
|
181
|
+
if (backend === "r2" && process.env.REPORT_UPLOAD_PRESIGNED === "true") {
|
|
182
|
+
return createPresignedUploader(process.env.PROJECT_NAME, process.env.TEST_RUN_GITHUB_ACTION_ID);
|
|
183
|
+
}
|
|
148
184
|
const enableS3 = backend === "s3";
|
|
149
185
|
const credentials = Uploader.getCredentialsFromEnv(enableS3);
|
|
150
186
|
if (!credentials) {
|
|
@@ -168,12 +204,12 @@ function createUploader() {
|
|
|
168
204
|
}
|
|
169
205
|
else {
|
|
170
206
|
const accountId = process.env.R2_ACCOUNT_ID;
|
|
171
|
-
const bucket =
|
|
207
|
+
const bucket = R2_REPORTS_BUCKET;
|
|
172
208
|
if (!accountId) {
|
|
173
209
|
logger_1.logger.debug("[Uploader] R2_ACCOUNT_ID not configured");
|
|
174
210
|
return null;
|
|
175
211
|
}
|
|
176
|
-
baseUrl =
|
|
212
|
+
baseUrl = R2_REPORTS_BASE_URL;
|
|
177
213
|
uploadBucket = bucket;
|
|
178
214
|
}
|
|
179
215
|
return new Uploader({
|
|
@@ -184,3 +220,16 @@ function createUploader() {
|
|
|
184
220
|
credentials,
|
|
185
221
|
});
|
|
186
222
|
}
|
|
223
|
+
function createPresignedUploader(projectSlug, runId) {
|
|
224
|
+
if (!process.env.EMPIRICALRUN_API_KEY) {
|
|
225
|
+
logger_1.logger.debug("[Uploader] EMPIRICALRUN_API_KEY is required for presigned uploads");
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return new Uploader({
|
|
229
|
+
projectSlug,
|
|
230
|
+
runId,
|
|
231
|
+
baseUrl: R2_REPORTS_BASE_URL,
|
|
232
|
+
uploadBucket: R2_REPORTS_BUCKET,
|
|
233
|
+
credentials: { provider: "r2-presigned", client: new api_worker_client_1.ApiWorkerClient() },
|
|
234
|
+
});
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22"
|
|
6
6
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"playwright-core": "1.63.0",
|
|
46
46
|
"serve-handler": "^6.1.6",
|
|
47
47
|
"typescript": "7.0.2",
|
|
48
|
-
"@empiricalrun/shared-types": "0.
|
|
48
|
+
"@empiricalrun/shared-types": "0.23.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"async-retry": "^1.3.3",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"ts-morph": "^28.0.0",
|
|
59
59
|
"@empiricalrun/cua": "^0.5.4",
|
|
60
60
|
"@empiricalrun/llm": "^0.28.3",
|
|
61
|
-
"@empiricalrun/r2-uploader": "^0.11.
|
|
61
|
+
"@empiricalrun/r2-uploader": "^0.11.5",
|
|
62
62
|
"@empiricalrun/reporter": "^0.30.3"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|