@allurereport/plugin-testops 3.16.0 → 3.16.1

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/dist/plugin.js CHANGED
@@ -9,31 +9,34 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- var _TestOpsPlugin_instances, _TestOpsPlugin_logger, _TestOpsPlugin_ci, _TestOpsPlugin_client, _TestOpsPlugin_launchName, _TestOpsPlugin_launchTags, _TestOpsPlugin_uploadedTestResultsIds, _TestOpsPlugin_uploadedGlobalAttachmentIds, _TestOpsPlugin_uploadedGlobalErrors, _TestOpsPlugin_autocloseLaunch, _TestOpsPlugin_launchStarted, _TestOpsPlugin_gitFlow, _TestOpsPlugin_enabledByConfig, _TestOpsPlugin_uploadQualityGateResults, _TestOpsPlugin_uploadGlobalErrors, _TestOpsPlugin_uploadGlobalAttachments, _TestOpsPlugin_uploadTestResults, _TestOpsPlugin_upload, _TestOpsPlugin_trsToUpload, _TestOpsPlugin_enrichWithCategories, _TestOpsPlugin_syncLaunchCategories, _TestOpsPlugin_collectCategoryNamesByExternalId, _TestOpsPlugin_assignCreatedCategoryIds, _TestOpsPlugin_startUpload, _TestOpsPlugin_stopUpload;
12
+ var _TestOpsPlugin_instances, _TestOpsPlugin_logger, _TestOpsPlugin_ci, _TestOpsPlugin_client, _TestOpsPlugin_launchName, _TestOpsPlugin_launchTags, _TestOpsPlugin_uploadedTestResultsIds, _TestOpsPlugin_uploadedGlobalAttachmentIds, _TestOpsPlugin_uploadedGlobalErrors, _TestOpsPlugin_autocloseLaunch, _TestOpsPlugin_launchStarted, _TestOpsPlugin_reopenClosedLaunch, _TestOpsPlugin_skippedUploadCycle, _TestOpsPlugin_launchId, _TestOpsPlugin_gitFlow, _TestOpsPlugin_enabledByConfig, _TestOpsPlugin_uploadQueue, _TestOpsPlugin_reopenLaunchIfClosed, _TestOpsPlugin_logDeferredUpload, _TestOpsPlugin_retryHooks, _TestOpsPlugin_logUploadFailure, _TestOpsPlugin_uploadQualityGateResults, _TestOpsPlugin_uploadGlobalErrors, _TestOpsPlugin_uploadGlobalAttachments, _TestOpsPlugin_uploadTestResults, _TestOpsPlugin_upload, _TestOpsPlugin_trsToUpload, _TestOpsPlugin_startUpload, _TestOpsPlugin_stopUpload;
13
13
  import { env } from "node:process";
14
- import { detect, isLocalCiDescriptor } from "@allurereport/ci";
14
+ import { applyAllureCiEnv, detect, isLocalCiDescriptor } from "@allurereport/ci";
15
15
  import { createProgressLogger } from "@allurereport/cli-commons";
16
16
  import { getWorstStatus } from "@allurereport/core-api";
17
17
  import { createPluginSummary, } from "@allurereport/plugin-api";
18
18
  import { uniqBy, stubTrue } from "lodash-es";
19
19
  import { bold } from "yoctocolors";
20
20
  import { TestOpsClient } from "./client.js";
21
+ import { isClosedLaunchError } from "./errors.js";
21
22
  import { LaunchGitFlow, resolveGitFlowOptions } from "./gitFlow/index.js";
22
23
  import { Logger } from "./logger.js";
24
+ import { UploadQueue } from "./uploadQueue.js";
23
25
  import { uploadFilenameForLink } from "./utils/attachments.js";
24
- import { toUploadCategory } from "./utils/categories.js";
25
- import { resolvePluginOptions } from "./utils/options.js";
26
- import { attachmentsResolverFactory, fixturesResolverFactory, unwrapStepsAttachments } from "./utils/resolvers.js";
26
+ import { applyCiOverrides } from "./utils/ciOverrides.js";
27
+ import { enrichWithCategories, syncLaunchCategories } from "./utils/launchCategories.js";
28
+ import { resolvePluginOptions, toPositiveInteger } from "./utils/options.js";
29
+ import { attachmentsResolverFactory, fixturesResolverFactory } from "./utils/resolvers.js";
27
30
  import { validateExecutableName } from "./utils/validation.js";
28
31
  const LAUNCH_PROGRESS_POLL_DELAY_MS = 500;
29
32
  const LAUNCH_PROGRESS_ATTEMPTS_LIMIT = 10;
30
- const categoryDisplayName = (cat) => cat.name ?? cat.grouping?.[0]?.name ?? cat.grouping?.[0]?.value ?? cat.grouping?.[0]?.key ?? cat.externalId;
33
+ const resolveJobRunIdFromEnv = () => toPositiveInteger(env.ALLURE_JOB_RUN_ID);
31
34
  export class TestOpsPlugin {
32
35
  constructor(options, context = {}) {
33
36
  _TestOpsPlugin_instances.add(this);
34
37
  this.options = options;
35
38
  _TestOpsPlugin_logger.set(this, new Logger("TestOpsPlugin"));
36
- _TestOpsPlugin_ci.set(this, detect());
39
+ _TestOpsPlugin_ci.set(this, void 0);
37
40
  _TestOpsPlugin_client.set(this, void 0);
38
41
  _TestOpsPlugin_launchName.set(this, "");
39
42
  _TestOpsPlugin_launchTags.set(this, []);
@@ -42,8 +45,14 @@ export class TestOpsPlugin {
42
45
  _TestOpsPlugin_uploadedGlobalErrors.set(this, new Set());
43
46
  _TestOpsPlugin_autocloseLaunch.set(this, false);
44
47
  _TestOpsPlugin_launchStarted.set(this, false);
48
+ _TestOpsPlugin_reopenClosedLaunch.set(this, false);
49
+ _TestOpsPlugin_skippedUploadCycle.set(this, false);
50
+ _TestOpsPlugin_launchId.set(this, void 0);
45
51
  _TestOpsPlugin_gitFlow.set(this, void 0);
46
52
  _TestOpsPlugin_enabledByConfig.set(this, false);
53
+ _TestOpsPlugin_uploadQueue.set(this, new UploadQueue());
54
+ applyAllureCiEnv();
55
+ __classPrivateFieldSet(this, _TestOpsPlugin_ci, applyCiOverrides(detect()), "f");
47
56
  __classPrivateFieldSet(this, _TestOpsPlugin_enabledByConfig, context.enabled === true, "f");
48
57
  if (context.enabled === false) {
49
58
  return;
@@ -52,17 +61,20 @@ export class TestOpsPlugin {
52
61
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`plugin is disabled - no CI environment detected. To enable, set ${bold("ALLURE_TESTOPS_ENABLED")}=true or ${bold("CI")}=true.`);
53
62
  return;
54
63
  }
55
- const { accessToken, endpoint, projectId, launchName, launchTags, autocloseLaunch = true, } = resolvePluginOptions(options);
64
+ const { accessToken, endpoint, projectId, launchName, launchTags, autocloseLaunch = true, uploadRateLimit, reopenClosedLaunch = false, launchId, } = resolvePluginOptions(options);
56
65
  if ([accessToken, endpoint, projectId].every(Boolean)) {
57
66
  __classPrivateFieldSet(this, _TestOpsPlugin_client, new TestOpsClient({
58
67
  baseUrl: endpoint,
59
68
  accessToken,
60
69
  projectId,
70
+ uploadRateLimit,
61
71
  }), "f");
62
72
  __classPrivateFieldSet(this, _TestOpsPlugin_launchName, launchName, "f");
63
73
  __classPrivateFieldSet(this, _TestOpsPlugin_launchTags, launchTags, "f");
64
74
  }
65
75
  __classPrivateFieldSet(this, _TestOpsPlugin_autocloseLaunch, autocloseLaunch, "f");
76
+ __classPrivateFieldSet(this, _TestOpsPlugin_reopenClosedLaunch, reopenClosedLaunch, "f");
77
+ __classPrivateFieldSet(this, _TestOpsPlugin_launchId, launchId, "f");
66
78
  const gitFlowOptions = resolveGitFlowOptions(options);
67
79
  __classPrivateFieldSet(this, _TestOpsPlugin_gitFlow, new LaunchGitFlow({
68
80
  ci: __classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"),
@@ -87,7 +99,7 @@ export class TestOpsPlugin {
87
99
  }
88
100
  return ["true", "1"].includes(value);
89
101
  };
90
- return isEnabled(env.ALLURE_TESTOPS_ENABLED) || isEnabled(env.CI);
102
+ return isEnabled(env.ALLURE_TESTOPS_ENABLED) || isEnabled(env.CI) || resolveJobRunIdFromEnv() !== undefined;
91
103
  }
92
104
  get isManuallyEnabled() {
93
105
  return __classPrivateFieldGet(this, _TestOpsPlugin_enabledByConfig, "f") || this.isOverridenByEnv;
@@ -144,6 +156,15 @@ export class TestOpsPlugin {
144
156
  const worstStatus = getWorstStatus(allTrs.map(({ status }) => status));
145
157
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("Finalizing upload…");
146
158
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_upload).call(this, store, { context, stage: "done" });
159
+ const stillPending = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").flush({
160
+ onRetry: async (error, attempt) => {
161
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(`Retrying deferred TestOps upload at finalization (attempt ${attempt}): ${error}`);
162
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_reopenLaunchIfClosed).call(this, error);
163
+ },
164
+ });
165
+ if (stillPending.length > 0) {
166
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`${stillPending.length} TestOps upload(s) could not be completed after retrying at finalization: ${stillPending.join(", ")}`);
167
+ }
147
168
  try {
148
169
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_stopUpload).call(this, worstStatus || "unknown");
149
170
  }
@@ -167,6 +188,10 @@ export class TestOpsPlugin {
167
188
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Upload finished. Allure TestOps Launch: ${__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchUrl}`);
168
189
  return;
169
190
  }
191
+ if (stillPending.length > 0 || __classPrivateFieldGet(this, _TestOpsPlugin_skippedUploadCycle, "f")) {
192
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Not closing launch ${launchId}: some uploads never made it to TestOps, closing now would report an incomplete launch as finished.`);
193
+ return;
194
+ }
170
195
  let launchIsReady = false;
171
196
  for (let attempt = 0; attempt < LAUNCH_PROGRESS_ATTEMPTS_LIMIT; attempt += 1) {
172
197
  launchIsReady = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").checkLaunchProgress();
@@ -214,7 +239,45 @@ export class TestOpsPlugin {
214
239
  return summary;
215
240
  }
216
241
  }
217
- _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalAttachmentIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalErrors = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_launchStarted = new WeakMap(), _TestOpsPlugin_gitFlow = new WeakMap(), _TestOpsPlugin_enabledByConfig = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_uploadQualityGateResults = async function _TestOpsPlugin_uploadQualityGateResults(store) {
242
+ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestOpsPlugin_client = new WeakMap(), _TestOpsPlugin_launchName = new WeakMap(), _TestOpsPlugin_launchTags = new WeakMap(), _TestOpsPlugin_uploadedTestResultsIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalAttachmentIds = new WeakMap(), _TestOpsPlugin_uploadedGlobalErrors = new WeakMap(), _TestOpsPlugin_autocloseLaunch = new WeakMap(), _TestOpsPlugin_launchStarted = new WeakMap(), _TestOpsPlugin_reopenClosedLaunch = new WeakMap(), _TestOpsPlugin_skippedUploadCycle = new WeakMap(), _TestOpsPlugin_launchId = new WeakMap(), _TestOpsPlugin_gitFlow = new WeakMap(), _TestOpsPlugin_enabledByConfig = new WeakMap(), _TestOpsPlugin_uploadQueue = new WeakMap(), _TestOpsPlugin_instances = new WeakSet(), _TestOpsPlugin_reopenLaunchIfClosed = async function _TestOpsPlugin_reopenLaunchIfClosed(error) {
243
+ if (!__classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f") || !isClosedLaunchError(error)) {
244
+ return;
245
+ }
246
+ const launchId = __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchId;
247
+ if (launchId === undefined) {
248
+ return;
249
+ }
250
+ try {
251
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Launch ${launchId} was closed - reopening before retrying the upload…`);
252
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").reopenLaunch(launchId);
253
+ }
254
+ catch (reopenError) {
255
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(`Failed to reopen launch ${launchId}: ${reopenError}`);
256
+ }
257
+ }, _TestOpsPlugin_logDeferredUpload = function _TestOpsPlugin_logDeferredUpload(label, error) {
258
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(error
259
+ ? `TestOps upload is unavailable; ${label} will be retried during finalization. Reason: ${error}`
260
+ : `TestOps upload is suspended; ${label} will be retried during finalization.`);
261
+ }, _TestOpsPlugin_retryHooks = function _TestOpsPlugin_retryHooks(label) {
262
+ return {
263
+ onRetry: async (error, attempt) => {
264
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(`Retrying ${label} upload (attempt ${attempt}): ${error}`);
265
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_reopenLaunchIfClosed).call(this, error);
266
+ },
267
+ onDeferred: (error) => __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logDeferredUpload).call(this, label, error),
268
+ };
269
+ }, _TestOpsPlugin_logUploadFailure = function _TestOpsPlugin_logUploadFailure(label, error) {
270
+ if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
271
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload ${label}: ${error.response.data.message}`);
272
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
273
+ }
274
+ else if (error instanceof Error) {
275
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload ${label}: ${error.message}`);
276
+ }
277
+ else {
278
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload ${label}`);
279
+ }
280
+ }, _TestOpsPlugin_uploadQualityGateResults = async function _TestOpsPlugin_uploadQualityGateResults(store) {
218
281
  const results = await store.qualityGateResults();
219
282
  const uniqueResults = uniqBy(results.filter(({ success }) => !success), ({ rule, environment }) => `${rule}-${environment}`);
220
283
  if (uniqueResults.length === 0) {
@@ -230,28 +293,22 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
230
293
  let completed = false;
231
294
  try {
232
295
  progressLogger.log(true);
233
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadQualityGateResults(uniqueResults, (percent) => {
296
+ const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run("quality-gate", () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadQualityGateResults(uniqueResults, (percent) => {
234
297
  if (!completed && percent >= 100) {
235
298
  completed = true;
236
299
  progressLogger.increment();
237
300
  }
238
- });
301
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "quality gate results"));
302
+ if (result.deferred) {
303
+ return;
304
+ }
239
305
  if (!completed) {
240
306
  progressLogger.increment();
241
307
  }
242
308
  progressLogger.log(true);
243
309
  }
244
310
  catch (error) {
245
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
246
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload quality gate results: ${error.response.data.message}`);
247
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
248
- }
249
- else if (error instanceof Error) {
250
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload quality gate results: ${error.message}`);
251
- }
252
- else {
253
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload quality gate results");
254
- }
311
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "quality gate results", error);
255
312
  }
256
313
  finally {
257
314
  progressLogger.cancel?.();
@@ -270,12 +327,15 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
270
327
  let completed = false;
271
328
  try {
272
329
  progressLogger.log(true);
273
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadGlobalErrors(results, (percent) => {
330
+ const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run("global-errors", () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadGlobalErrors(results, (percent) => {
274
331
  if (!completed && percent >= 100) {
275
332
  completed = true;
276
333
  progressLogger.increment();
277
334
  }
278
- });
335
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "global errors"));
336
+ if (result.deferred) {
337
+ return;
338
+ }
279
339
  if (!completed) {
280
340
  progressLogger.increment();
281
341
  }
@@ -285,16 +345,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
285
345
  progressLogger.log(true);
286
346
  }
287
347
  catch (error) {
288
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
289
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global errors: ${error.response.data.message}`);
290
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
291
- }
292
- else if (error instanceof Error) {
293
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global errors: ${error.message}`);
294
- }
295
- else {
296
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload global errors");
297
- }
348
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "global errors", error);
298
349
  }
299
350
  finally {
300
351
  progressLogger.cancel?.();
@@ -313,7 +364,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
313
364
  let completed = false;
314
365
  try {
315
366
  progressLogger.log(true);
316
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadGlobalAttachments({
367
+ const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run("global-attachments", () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadGlobalAttachments({
317
368
  attachments,
318
369
  attachmentsResolver: async (attachmentLink) => {
319
370
  const content = await store.attachmentContentById(attachmentLink.id);
@@ -326,6 +377,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
326
377
  originalFileName: filename,
327
378
  contentType: attachmentLink.contentType ?? "application/octet-stream",
328
379
  content: body,
380
+ contentLength: content?.getContentLength(),
329
381
  };
330
382
  },
331
383
  onProgress: (percent) => {
@@ -334,7 +386,10 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
334
386
  progressLogger.increment();
335
387
  }
336
388
  },
337
- });
389
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "global attachments"));
390
+ if (result.deferred) {
391
+ return;
392
+ }
338
393
  if (!completed) {
339
394
  progressLogger.increment();
340
395
  }
@@ -344,16 +399,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
344
399
  progressLogger.log(true);
345
400
  }
346
401
  catch (error) {
347
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
348
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global attachments: ${error.response.data.message}`);
349
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
350
- }
351
- else if (error instanceof Error) {
352
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global attachments: ${error.message}`);
353
- }
354
- else {
355
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload global attachments");
356
- }
402
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "global attachments", error);
357
403
  }
358
404
  finally {
359
405
  progressLogger.cancel?.();
@@ -370,13 +416,22 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
370
416
  });
371
417
  try {
372
418
  progressLogger.log(true);
373
- const uploadedTrs = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadTestResults({
419
+ const { onRetry, onDeferred } = __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "test results");
420
+ const result = await __classPrivateFieldGet(this, _TestOpsPlugin_uploadQueue, "f").run("test-results", () => __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadTestResults({
374
421
  attachmentsResolver: attachmentsResolverFactory(store),
375
422
  fixturesResolver: fixturesResolverFactory(store),
376
423
  environments,
377
- trs: trsToUpload,
424
+ trs: trsToUpload.filter((tr) => !__classPrivateFieldGet(this, _TestOpsPlugin_uploadedTestResultsIds, "f").has(tr.id)),
378
425
  onProgress: () => progressLogger.increment(),
379
- });
426
+ onRetry,
427
+ onChunkUploaded: (uploadedChunkTrs) => {
428
+ uploadedChunkTrs.forEach((tr) => __classPrivateFieldGet(this, _TestOpsPlugin_uploadedTestResultsIds, "f").add(tr.id));
429
+ },
430
+ }), { onRetry, onDeferred });
431
+ if (result.deferred) {
432
+ return;
433
+ }
434
+ const uploadedTrs = result.value;
380
435
  progressLogger.log(true);
381
436
  uploadedTrs.forEach((tr) => {
382
437
  __classPrivateFieldGet(this, _TestOpsPlugin_uploadedTestResultsIds, "f").add(tr.id);
@@ -386,7 +441,10 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
386
441
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No test results were uploaded");
387
442
  return;
388
443
  }
389
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Uploaded ${uploadedCount} ${uploadedCount > 1 ? "test results" : "test result"}`);
444
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Uploaded ${uploadedCount} ${uploadedCount > 1 ? "test results" : "test result"}`);
445
+ }
446
+ catch (error) {
447
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "test results", error);
390
448
  }
391
449
  finally {
392
450
  progressLogger.cancel?.();
@@ -420,9 +478,10 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
420
478
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Found ${bold(trsToUpload.length.toString())} new test ${trsToUpload.length > 1 ? "results" : "result"}, uploading…`);
421
479
  }
422
480
  try {
423
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession(env);
481
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession(env, __classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f"));
424
482
  }
425
483
  catch (error) {
484
+ __classPrivateFieldSet(this, _TestOpsPlugin_skippedUploadCycle, true, "f");
426
485
  if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
427
486
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to create TestOps session: ${error.response.data.message}`);
428
487
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
@@ -435,6 +494,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
435
494
  }
436
495
  return;
437
496
  }
497
+ __classPrivateFieldSet(this, _TestOpsPlugin_skippedUploadCycle, false, "f");
438
498
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalAttachments).call(this, store, globalAttachments);
439
499
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalErrors).call(this, globalErrors);
440
500
  if (trsToUpload.length === 0) {
@@ -443,8 +503,11 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
443
503
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadQualityGateResults).call(this, store);
444
504
  const environments = await store.allEnvironmentIdentities();
445
505
  const contextCategories = context?.categories ?? [];
446
- const trsEnrichedWithCategories = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_enrichWithCategories).call(this, store, trsToUpload, contextCategories);
447
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_syncLaunchCategories).call(this, trsEnrichedWithCategories, contextCategories);
506
+ const trsEnrichedWithCategories = await enrichWithCategories(store, trsToUpload, contextCategories);
507
+ const categoriesError = await syncLaunchCategories(__classPrivateFieldGet(this, _TestOpsPlugin_client, "f"), trsEnrichedWithCategories, contextCategories);
508
+ if (categoriesError) {
509
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn(`Uploading test results without launch categories: ${categoriesError.message}`);
510
+ }
448
511
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, trsEnrichedWithCategories, environments, {
449
512
  silent: !!context?.realTime,
450
513
  });
@@ -461,96 +524,19 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
461
524
  includeRetries: false,
462
525
  });
463
526
  return filteredTrs;
464
- }, _TestOpsPlugin_enrichWithCategories = async function _TestOpsPlugin_enrichWithCategories(store, trs, contextCategories) {
465
- return Promise.all(trs.map(async (tr) => {
466
- const environmentId = await store.environmentIdByTrId(tr.id);
467
- const base = {
468
- ...tr,
469
- ...(environmentId ? { environment: environmentId } : {}),
470
- steps: unwrapStepsAttachments(tr.steps),
471
- };
472
- const category = toUploadCategory(base, contextCategories ?? []);
473
- if (category) {
474
- base.category = category;
475
- }
476
- return base;
477
- }));
478
- }, _TestOpsPlugin_syncLaunchCategories = async function _TestOpsPlugin_syncLaunchCategories(trs, contextCategories) {
479
- const categoryNamesByExternalId = __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_collectCategoryNamesByExternalId).call(this, trs);
480
- if (categoryNamesByExternalId.size === 0) {
481
- return;
482
- }
483
- const bulkItems = [];
484
- const seenExternalIds = new Set();
485
- for (const tr of trs) {
486
- const cat = tr.category;
487
- if (!cat?.externalId)
488
- continue;
489
- if (seenExternalIds.has(cat.externalId))
490
- continue;
491
- seenExternalIds.add(cat.externalId);
492
- bulkItems.push({
493
- externalId: cat.externalId,
494
- name: categoryNamesByExternalId.get(cat.externalId) ?? categoryDisplayName(cat),
495
- hide: cat.hide,
496
- expand: cat.expand,
497
- });
498
- }
499
- const rankByExternalId = new Map();
500
- for (const c of contextCategories) {
501
- if (!rankByExternalId.has(c.id)) {
502
- rankByExternalId.set(c.id, c.index);
503
- }
504
- if (!rankByExternalId.has(c.name)) {
505
- rankByExternalId.set(c.name, c.index);
506
- }
507
- }
508
- const ranked = bulkItems.map((item, i) => ({
509
- item,
510
- i,
511
- rank: rankByExternalId.get(item.externalId),
512
- }));
513
- ranked.sort((a, b) => {
514
- const ar = a.rank ?? Number.POSITIVE_INFINITY;
515
- const br = b.rank ?? Number.POSITIVE_INFINITY;
516
- if (ar !== br)
517
- return ar - br;
518
- return a.i - b.i;
519
- });
520
- const orderedBulkItems = ranked.map((r) => r.item);
521
- const launchId = __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchId;
522
- try {
523
- const created = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunchCategoriesBulk(launchId, orderedBulkItems);
524
- const categoryIdByExternalId = new Map(created.map((r) => [r.externalId, r.id]));
525
- __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_assignCreatedCategoryIds).call(this, trs, categoryIdByExternalId);
526
- }
527
- catch {
528
- }
529
- }, _TestOpsPlugin_collectCategoryNamesByExternalId = function _TestOpsPlugin_collectCategoryNamesByExternalId(trs) {
530
- const map = new Map();
531
- for (const tr of trs) {
532
- const cat = tr.category;
533
- if (cat?.externalId) {
534
- map.set(cat.externalId, categoryDisplayName(cat));
535
- }
536
- }
537
- return map;
538
- }, _TestOpsPlugin_assignCreatedCategoryIds = function _TestOpsPlugin_assignCreatedCategoryIds(trs, idByExternalId) {
539
- for (const tr of trs) {
540
- const cat = tr.category;
541
- if (!cat?.externalId) {
542
- continue;
543
- }
544
- const id = idByExternalId.get(cat.externalId);
545
- if (typeof id === "number") {
546
- tr.category = { ...cat, id };
547
- }
548
- }
549
527
  }, _TestOpsPlugin_startUpload = async function _TestOpsPlugin_startUpload() {
550
- const launchGitContext = __classPrivateFieldGet(this, _TestOpsPlugin_gitFlow, "f").resolve();
528
+ const jobRunId = resolveJobRunIdFromEnv();
551
529
  try {
552
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunch(__classPrivateFieldGet(this, _TestOpsPlugin_launchName, "f"), __classPrivateFieldGet(this, _TestOpsPlugin_launchTags, "f"), launchGitContext);
553
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").startUpload(__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"));
530
+ if (!jobRunId) {
531
+ if (__classPrivateFieldGet(this, _TestOpsPlugin_launchId, "f") !== undefined) {
532
+ __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").attachToLaunch(__classPrivateFieldGet(this, _TestOpsPlugin_launchId, "f"));
533
+ }
534
+ else {
535
+ const launchGitContext = __classPrivateFieldGet(this, _TestOpsPlugin_gitFlow, "f").resolve();
536
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunch(__classPrivateFieldGet(this, _TestOpsPlugin_launchName, "f"), __classPrivateFieldGet(this, _TestOpsPlugin_launchTags, "f"), launchGitContext);
537
+ }
538
+ }
539
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").startUpload(__classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"), jobRunId);
554
540
  __classPrivateFieldSet(this, _TestOpsPlugin_launchStarted, true, "f");
555
541
  }
556
542
  catch (error) {
@@ -0,0 +1,12 @@
1
+ import type { UploadRateLimit } from "./model.js";
2
+ export type UploadCost = {
3
+ requests?: number;
4
+ files?: number;
5
+ bytes?: number;
6
+ };
7
+ export declare const DEFAULT_UPLOAD_RATE_LIMIT: UploadRateLimit;
8
+ export declare class UploadPacer {
9
+ #private;
10
+ constructor(rateLimit: UploadRateLimit | false | undefined, now?: () => number);
11
+ wait(cost: UploadCost, signal?: AbortSignal): Promise<void>;
12
+ }
@@ -0,0 +1,76 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _UploadPacer_instances, _UploadPacer_requests, _UploadPacer_files, _UploadPacer_bytes, _UploadPacer_now, _UploadPacer_reserve, _UploadPacer_reserveAt;
13
+ import { setTimeout as delay } from "node:timers/promises";
14
+ export const DEFAULT_UPLOAD_RATE_LIMIT = {
15
+ windowMs: 1000,
16
+ maxRequestsPerWindow: 20,
17
+ maxFilesPerWindow: 1000,
18
+ maxBytesPerWindow: 1024 * 1024 * 1024,
19
+ };
20
+ const isBudgetEnabled = (budget) => budget.windowMs > 0 && budget.limit > 0;
21
+ const scaledWindowCost = (windowMs, cost, limit) => {
22
+ if (windowMs <= 0 || cost <= 0 || limit <= 0) {
23
+ return 0;
24
+ }
25
+ return (windowMs * cost) / limit;
26
+ };
27
+ const resolveRateLimit = (rateLimit) => {
28
+ const rateLimitUnset = rateLimit === undefined;
29
+ const rateLimitDisabled = rateLimit === false;
30
+ if (rateLimitUnset) {
31
+ return DEFAULT_UPLOAD_RATE_LIMIT;
32
+ }
33
+ if (rateLimitDisabled) {
34
+ return undefined;
35
+ }
36
+ return rateLimit;
37
+ };
38
+ export class UploadPacer {
39
+ constructor(rateLimit, now = Date.now) {
40
+ _UploadPacer_instances.add(this);
41
+ _UploadPacer_requests.set(this, void 0);
42
+ _UploadPacer_files.set(this, void 0);
43
+ _UploadPacer_bytes.set(this, void 0);
44
+ _UploadPacer_now.set(this, void 0);
45
+ const resolvedRateLimit = resolveRateLimit(rateLimit);
46
+ const windowMs = resolvedRateLimit?.windowMs ?? 0;
47
+ __classPrivateFieldSet(this, _UploadPacer_requests, { limit: resolvedRateLimit?.maxRequestsPerWindow ?? 0, windowMs, next: 0 }, "f");
48
+ __classPrivateFieldSet(this, _UploadPacer_files, { limit: resolvedRateLimit?.maxFilesPerWindow ?? 0, windowMs, next: 0 }, "f");
49
+ __classPrivateFieldSet(this, _UploadPacer_bytes, { limit: resolvedRateLimit?.maxBytesPerWindow ?? 0, windowMs, next: 0 }, "f");
50
+ __classPrivateFieldSet(this, _UploadPacer_now, now, "f");
51
+ }
52
+ async wait(cost, signal) {
53
+ const waitMs = __classPrivateFieldGet(this, _UploadPacer_instances, "m", _UploadPacer_reserve).call(this, __classPrivateFieldGet(this, _UploadPacer_now, "f").call(this), cost);
54
+ if (waitMs <= 0) {
55
+ return;
56
+ }
57
+ await delay(waitMs, undefined, signal ? { signal } : undefined);
58
+ }
59
+ }
60
+ _UploadPacer_requests = new WeakMap(), _UploadPacer_files = new WeakMap(), _UploadPacer_bytes = new WeakMap(), _UploadPacer_now = new WeakMap(), _UploadPacer_instances = new WeakSet(), _UploadPacer_reserve = function _UploadPacer_reserve(now, cost) {
61
+ let start = now;
62
+ for (const budget of [__classPrivateFieldGet(this, _UploadPacer_requests, "f"), __classPrivateFieldGet(this, _UploadPacer_files, "f"), __classPrivateFieldGet(this, _UploadPacer_bytes, "f")]) {
63
+ if (isBudgetEnabled(budget) && budget.next > start) {
64
+ start = budget.next;
65
+ }
66
+ }
67
+ __classPrivateFieldGet(this, _UploadPacer_instances, "m", _UploadPacer_reserveAt).call(this, __classPrivateFieldGet(this, _UploadPacer_requests, "f"), start, cost.requests ?? 0);
68
+ __classPrivateFieldGet(this, _UploadPacer_instances, "m", _UploadPacer_reserveAt).call(this, __classPrivateFieldGet(this, _UploadPacer_files, "f"), start, cost.files ?? 0);
69
+ __classPrivateFieldGet(this, _UploadPacer_instances, "m", _UploadPacer_reserveAt).call(this, __classPrivateFieldGet(this, _UploadPacer_bytes, "f"), start, cost.bytes ?? 0);
70
+ return start > now ? start - now : 0;
71
+ }, _UploadPacer_reserveAt = function _UploadPacer_reserveAt(budget, start, cost) {
72
+ if (!isBudgetEnabled(budget) || cost <= 0) {
73
+ return;
74
+ }
75
+ budget.next = start + scaledWindowCost(budget.windowMs, cost, budget.limit);
76
+ };
@@ -0,0 +1,15 @@
1
+ import { type RetryOptions } from "./errors.js";
2
+ export type UploadRunResult<T> = {
3
+ deferred: false;
4
+ value: T;
5
+ } | {
6
+ deferred: true;
7
+ };
8
+ export declare class UploadQueue {
9
+ #private;
10
+ get pendingNames(): string[];
11
+ run<T>(name: string, task: () => Promise<T>, options?: RetryOptions & {
12
+ onDeferred?: (error?: unknown) => void;
13
+ }): Promise<UploadRunResult<T>>;
14
+ flush(retryOptions?: RetryOptions): Promise<string[]>;
15
+ }
@@ -0,0 +1,61 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _UploadQueue_pending, _UploadQueue_suspended;
13
+ import { ErrorKind, classifyError, shouldRetryUpload, withUploadRetry } from "./errors.js";
14
+ const isSuspendingError = (error) => classifyError(error) === ErrorKind.ResourceRecoverable;
15
+ export class UploadQueue {
16
+ constructor() {
17
+ _UploadQueue_pending.set(this, new Map());
18
+ _UploadQueue_suspended.set(this, false);
19
+ }
20
+ get pendingNames() {
21
+ return [...__classPrivateFieldGet(this, _UploadQueue_pending, "f").keys()];
22
+ }
23
+ async run(name, task, options = {}) {
24
+ const { onDeferred, ...retryOptions } = options;
25
+ if (__classPrivateFieldGet(this, _UploadQueue_suspended, "f")) {
26
+ __classPrivateFieldGet(this, _UploadQueue_pending, "f").set(name, task);
27
+ onDeferred?.();
28
+ return { deferred: true };
29
+ }
30
+ try {
31
+ const value = await withUploadRetry(task, retryOptions);
32
+ __classPrivateFieldGet(this, _UploadQueue_pending, "f").delete(name);
33
+ return { deferred: false, value };
34
+ }
35
+ catch (error) {
36
+ if (!shouldRetryUpload(error)) {
37
+ throw error;
38
+ }
39
+ __classPrivateFieldGet(this, _UploadQueue_pending, "f").set(name, task);
40
+ __classPrivateFieldSet(this, _UploadQueue_suspended, isSuspendingError(error), "f");
41
+ onDeferred?.(error);
42
+ return { deferred: true };
43
+ }
44
+ }
45
+ async flush(retryOptions = {}) {
46
+ const remaining = new Map(__classPrivateFieldGet(this, _UploadQueue_pending, "f"));
47
+ for (const [name, task] of __classPrivateFieldGet(this, _UploadQueue_pending, "f")) {
48
+ try {
49
+ await withUploadRetry(task, retryOptions);
50
+ remaining.delete(name);
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ }
56
+ __classPrivateFieldSet(this, _UploadQueue_pending, remaining, "f");
57
+ __classPrivateFieldSet(this, _UploadQueue_suspended, remaining.size > 0, "f");
58
+ return [...remaining.keys()];
59
+ }
60
+ }
61
+ _UploadQueue_pending = new WeakMap(), _UploadQueue_suspended = new WeakMap();
@@ -0,0 +1,2 @@
1
+ import type { CiDescriptor } from "@allurereport/core-api";
2
+ export declare const applyCiOverrides: (ci: CiDescriptor) => CiDescriptor;