@allurereport/plugin-testops 3.15.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,39 +9,50 @@ 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_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, []);
40
43
  _TestOpsPlugin_uploadedTestResultsIds.set(this, new Set());
44
+ _TestOpsPlugin_uploadedGlobalAttachmentIds.set(this, new Set());
45
+ _TestOpsPlugin_uploadedGlobalErrors.set(this, new Set());
41
46
  _TestOpsPlugin_autocloseLaunch.set(this, false);
42
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);
43
51
  _TestOpsPlugin_gitFlow.set(this, void 0);
44
52
  _TestOpsPlugin_enabledByConfig.set(this, false);
53
+ _TestOpsPlugin_uploadQueue.set(this, new UploadQueue());
54
+ applyAllureCiEnv();
55
+ __classPrivateFieldSet(this, _TestOpsPlugin_ci, applyCiOverrides(detect()), "f");
45
56
  __classPrivateFieldSet(this, _TestOpsPlugin_enabledByConfig, context.enabled === true, "f");
46
57
  if (context.enabled === false) {
47
58
  return;
@@ -50,17 +61,20 @@ export class TestOpsPlugin {
50
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.`);
51
62
  return;
52
63
  }
53
- 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);
54
65
  if ([accessToken, endpoint, projectId].every(Boolean)) {
55
66
  __classPrivateFieldSet(this, _TestOpsPlugin_client, new TestOpsClient({
56
67
  baseUrl: endpoint,
57
68
  accessToken,
58
69
  projectId,
70
+ uploadRateLimit,
59
71
  }), "f");
60
72
  __classPrivateFieldSet(this, _TestOpsPlugin_launchName, launchName, "f");
61
73
  __classPrivateFieldSet(this, _TestOpsPlugin_launchTags, launchTags, "f");
62
74
  }
63
75
  __classPrivateFieldSet(this, _TestOpsPlugin_autocloseLaunch, autocloseLaunch, "f");
76
+ __classPrivateFieldSet(this, _TestOpsPlugin_reopenClosedLaunch, reopenClosedLaunch, "f");
77
+ __classPrivateFieldSet(this, _TestOpsPlugin_launchId, launchId, "f");
64
78
  const gitFlowOptions = resolveGitFlowOptions(options);
65
79
  __classPrivateFieldSet(this, _TestOpsPlugin_gitFlow, new LaunchGitFlow({
66
80
  ci: __classPrivateFieldGet(this, _TestOpsPlugin_ci, "f"),
@@ -85,7 +99,7 @@ export class TestOpsPlugin {
85
99
  }
86
100
  return ["true", "1"].includes(value);
87
101
  };
88
- return isEnabled(env.ALLURE_TESTOPS_ENABLED) || isEnabled(env.CI);
102
+ return isEnabled(env.ALLURE_TESTOPS_ENABLED) || isEnabled(env.CI) || resolveJobRunIdFromEnv() !== undefined;
89
103
  }
90
104
  get isManuallyEnabled() {
91
105
  return __classPrivateFieldGet(this, _TestOpsPlugin_enabledByConfig, "f") || this.isOverridenByEnv;
@@ -106,6 +120,9 @@ export class TestOpsPlugin {
106
120
  if (!this.enabled) {
107
121
  return;
108
122
  }
123
+ if (context.realTime) {
124
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").setLogLevel("info");
125
+ }
109
126
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("Starting upload…");
110
127
  if (!(await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_startUpload).call(this))) {
111
128
  return;
@@ -139,6 +156,15 @@ export class TestOpsPlugin {
139
156
  const worstStatus = getWorstStatus(allTrs.map(({ status }) => status));
140
157
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("Finalizing upload…");
141
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
+ }
142
168
  try {
143
169
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_stopUpload).call(this, worstStatus || "unknown");
144
170
  }
@@ -162,6 +188,10 @@ export class TestOpsPlugin {
162
188
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Upload finished. Allure TestOps Launch: ${__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchUrl}`);
163
189
  return;
164
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
+ }
165
195
  let launchIsReady = false;
166
196
  for (let attempt = 0; attempt < LAUNCH_PROGRESS_ATTEMPTS_LIMIT; attempt += 1) {
167
197
  launchIsReady = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").checkLaunchProgress();
@@ -209,7 +239,45 @@ export class TestOpsPlugin {
209
239
  return summary;
210
240
  }
211
241
  }
212
- _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_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) {
213
281
  const results = await store.qualityGateResults();
214
282
  const uniqueResults = uniqBy(results.filter(({ success }) => !success), ({ rule, environment }) => `${rule}-${environment}`);
215
283
  if (uniqueResults.length === 0) {
@@ -225,36 +293,29 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
225
293
  let completed = false;
226
294
  try {
227
295
  progressLogger.log(true);
228
- 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) => {
229
297
  if (!completed && percent >= 100) {
230
298
  completed = true;
231
299
  progressLogger.increment();
232
300
  }
233
- });
301
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "quality gate results"));
302
+ if (result.deferred) {
303
+ return;
304
+ }
234
305
  if (!completed) {
235
306
  progressLogger.increment();
236
307
  }
237
308
  progressLogger.log(true);
238
309
  }
239
310
  catch (error) {
240
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
241
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload quality gate results: ${error.response.data.message}`);
242
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
243
- }
244
- else if (error instanceof Error) {
245
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload quality gate results: ${error.message}`);
246
- }
247
- else {
248
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload quality gate results");
249
- }
311
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "quality gate results", error);
250
312
  }
251
313
  finally {
252
314
  progressLogger.cancel?.();
253
315
  }
254
- }, _TestOpsPlugin_uploadGlobalErrors = async function _TestOpsPlugin_uploadGlobalErrors(store) {
255
- const results = await store.allGlobalErrors();
316
+ }, _TestOpsPlugin_uploadGlobalErrors = async function _TestOpsPlugin_uploadGlobalErrors(results) {
256
317
  if (results.length === 0) {
257
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No global errors to upload");
318
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No new global errors to upload");
258
319
  return;
259
320
  }
260
321
  const progressLogger = createProgressLogger({
@@ -266,36 +327,32 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
266
327
  let completed = false;
267
328
  try {
268
329
  progressLogger.log(true);
269
- 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) => {
270
331
  if (!completed && percent >= 100) {
271
332
  completed = true;
272
333
  progressLogger.increment();
273
334
  }
274
- });
335
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "global errors"));
336
+ if (result.deferred) {
337
+ return;
338
+ }
275
339
  if (!completed) {
276
340
  progressLogger.increment();
277
341
  }
342
+ results.forEach((error) => {
343
+ __classPrivateFieldGet(this, _TestOpsPlugin_uploadedGlobalErrors, "f").add(error);
344
+ });
278
345
  progressLogger.log(true);
279
346
  }
280
347
  catch (error) {
281
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
282
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global errors: ${error.response.data.message}`);
283
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
284
- }
285
- else if (error instanceof Error) {
286
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global errors: ${error.message}`);
287
- }
288
- else {
289
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload global errors");
290
- }
348
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "global errors", error);
291
349
  }
292
350
  finally {
293
351
  progressLogger.cancel?.();
294
352
  }
295
- }, _TestOpsPlugin_uploadGlobalAttachments = async function _TestOpsPlugin_uploadGlobalAttachments(store) {
296
- const attachments = await store.allGlobalAttachments();
353
+ }, _TestOpsPlugin_uploadGlobalAttachments = async function _TestOpsPlugin_uploadGlobalAttachments(store, attachments) {
297
354
  if (attachments.length === 0) {
298
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug("No global attachments to upload");
355
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug("No new global attachments to upload");
299
356
  return;
300
357
  }
301
358
  const progressLogger = createProgressLogger({
@@ -307,7 +364,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
307
364
  let completed = false;
308
365
  try {
309
366
  progressLogger.log(true);
310
- 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({
311
368
  attachments,
312
369
  attachmentsResolver: async (attachmentLink) => {
313
370
  const content = await store.attachmentContentById(attachmentLink.id);
@@ -320,6 +377,7 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
320
377
  originalFileName: filename,
321
378
  contentType: attachmentLink.contentType ?? "application/octet-stream",
322
379
  content: body,
380
+ contentLength: content?.getContentLength(),
323
381
  };
324
382
  },
325
383
  onProgress: (percent) => {
@@ -328,77 +386,102 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
328
386
  progressLogger.increment();
329
387
  }
330
388
  },
331
- });
389
+ }), __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_retryHooks).call(this, "global attachments"));
390
+ if (result.deferred) {
391
+ return;
392
+ }
332
393
  if (!completed) {
333
394
  progressLogger.increment();
334
395
  }
396
+ attachments.forEach((attachment) => {
397
+ __classPrivateFieldGet(this, _TestOpsPlugin_uploadedGlobalAttachmentIds, "f").add(attachment.id);
398
+ });
335
399
  progressLogger.log(true);
336
400
  }
337
401
  catch (error) {
338
- if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
339
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global attachments: ${error.response.data.message}`);
340
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
341
- }
342
- else if (error instanceof Error) {
343
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to upload global attachments: ${error.message}`);
344
- }
345
- else {
346
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error("Failed to upload global attachments");
347
- }
402
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "global attachments", error);
348
403
  }
349
404
  finally {
350
405
  progressLogger.cancel?.();
351
406
  }
352
- }, _TestOpsPlugin_uploadTestResults = async function _TestOpsPlugin_uploadTestResults(store, trsToUpload, environments) {
407
+ }, _TestOpsPlugin_uploadTestResults = async function _TestOpsPlugin_uploadTestResults(store, trsToUpload, environments, options) {
353
408
  const totalCount = trsToUpload.length;
354
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Preparing to upload ${bold(totalCount.toString())} ${totalCount > 1 ? "test results" : "test result"}`);
409
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Preparing to upload ${bold(totalCount.toString())} ${totalCount > 1 ? "test results" : "test result"}`);
355
410
  const progressLogger = createProgressLogger({
356
411
  total: totalCount,
357
412
  message: "Uploading test results",
358
413
  unitLabel: totalCount === 1 ? "test result uploaded" : "test results uploaded",
359
414
  prefix: "[TestOpsPlugin]",
415
+ silent: !!options?.silent,
360
416
  });
361
- const logProgress = progressLogger.log;
362
- const incrementProgress = progressLogger.increment;
363
417
  try {
364
- logProgress(true);
365
- const uploadedTrs = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").uploadTestResults({
418
+ progressLogger.log(true);
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({
366
421
  attachmentsResolver: attachmentsResolverFactory(store),
367
422
  fixturesResolver: fixturesResolverFactory(store),
368
423
  environments,
369
- trs: trsToUpload,
370
- onProgress: () => incrementProgress(),
371
- });
372
- logProgress(true);
424
+ trs: trsToUpload.filter((tr) => !__classPrivateFieldGet(this, _TestOpsPlugin_uploadedTestResultsIds, "f").has(tr.id)),
425
+ onProgress: () => progressLogger.increment(),
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;
435
+ progressLogger.log(true);
373
436
  uploadedTrs.forEach((tr) => {
374
437
  __classPrivateFieldGet(this, _TestOpsPlugin_uploadedTestResultsIds, "f").add(tr.id);
375
438
  });
376
439
  const uploadedCount = uploadedTrs.length;
377
440
  if (uploadedCount === 0) {
378
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").warn("No test results were uploaded");
441
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No test results were uploaded");
379
442
  return;
380
443
  }
381
444
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info(`Uploaded ${uploadedCount} ${uploadedCount > 1 ? "test results" : "test result"}`);
382
445
  }
446
+ catch (error) {
447
+ __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_logUploadFailure).call(this, "test results", error);
448
+ }
383
449
  finally {
384
450
  progressLogger.cancel?.();
385
451
  }
386
452
  }, _TestOpsPlugin_upload = async function _TestOpsPlugin_upload(store, options = {}) {
387
453
  const { context, stage } = options;
388
454
  const trsToUpload = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_trsToUpload).call(this, store);
455
+ const shouldUploadGlobalArtifacts = stage === "done" || !context?.realTime;
456
+ let globalErrors = [];
457
+ let globalAttachments = [];
458
+ if (shouldUploadGlobalArtifacts) {
459
+ const [allGlobalErrors, allGlobalAttachments] = await Promise.all([
460
+ store.allGlobalErrors(),
461
+ store.allGlobalAttachments(),
462
+ ]);
463
+ globalErrors = allGlobalErrors.filter((error) => !__classPrivateFieldGet(this, _TestOpsPlugin_uploadedGlobalErrors, "f").has(error));
464
+ globalAttachments = allGlobalAttachments.filter((attachment) => !__classPrivateFieldGet(this, _TestOpsPlugin_uploadedGlobalAttachmentIds, "f").has(attachment.id));
465
+ }
389
466
  if (trsToUpload.length === 0) {
390
467
  if (stage == "update") {
391
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info("No new test results to upload");
468
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No new test results to upload");
392
469
  }
393
470
  if (stage === "done") {
394
- __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").info("No test results to upload");
471
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose("No test results to upload");
395
472
  }
396
- return;
473
+ if (globalErrors.length === 0 && globalAttachments.length === 0) {
474
+ return;
475
+ }
476
+ }
477
+ if (stage === "update" && trsToUpload.length > 0) {
478
+ __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").verbose(`Found ${bold(trsToUpload.length.toString())} new test ${trsToUpload.length > 1 ? "results" : "result"}, uploading…`);
397
479
  }
398
480
  try {
399
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession(env);
481
+ await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createSession(env, __classPrivateFieldGet(this, _TestOpsPlugin_reopenClosedLaunch, "f"));
400
482
  }
401
483
  catch (error) {
484
+ __classPrivateFieldSet(this, _TestOpsPlugin_skippedUploadCycle, true, "f");
402
485
  if (__classPrivateFieldGet(this, _TestOpsPlugin_client, "f").isTestOpsClientError(error)) {
403
486
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").error(`Failed to create TestOps session: ${error.response.data.message}`);
404
487
  __classPrivateFieldGet(this, _TestOpsPlugin_logger, "f").debug(error.response?.data);
@@ -411,14 +494,23 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
411
494
  }
412
495
  return;
413
496
  }
414
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalAttachments).call(this, store);
415
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalErrors).call(this, store);
497
+ __classPrivateFieldSet(this, _TestOpsPlugin_skippedUploadCycle, false, "f");
498
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalAttachments).call(this, store, globalAttachments);
499
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadGlobalErrors).call(this, globalErrors);
500
+ if (trsToUpload.length === 0) {
501
+ return;
502
+ }
416
503
  await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadQualityGateResults).call(this, store);
417
504
  const environments = await store.allEnvironmentIdentities();
418
505
  const contextCategories = context?.categories ?? [];
419
- const trsEnrichedWithCategories = await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_enrichWithCategories).call(this, store, trsToUpload, contextCategories);
420
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_syncLaunchCategories).call(this, trsEnrichedWithCategories, contextCategories);
421
- await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, trsEnrichedWithCategories, environments);
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
+ }
511
+ await __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_uploadTestResults).call(this, store, trsEnrichedWithCategories, environments, {
512
+ silent: !!context?.realTime,
513
+ });
422
514
  }, _TestOpsPlugin_trsToUpload = async function _TestOpsPlugin_trsToUpload(store) {
423
515
  const filter = this.options.filter ?? stubTrue;
424
516
  const filteredTrs = await store.allTestResults({
@@ -432,96 +524,19 @@ _TestOpsPlugin_logger = new WeakMap(), _TestOpsPlugin_ci = new WeakMap(), _TestO
432
524
  includeRetries: false,
433
525
  });
434
526
  return filteredTrs;
435
- }, _TestOpsPlugin_enrichWithCategories = async function _TestOpsPlugin_enrichWithCategories(store, trs, contextCategories) {
436
- return Promise.all(trs.map(async (tr) => {
437
- const environmentId = await store.environmentIdByTrId(tr.id);
438
- const base = {
439
- ...tr,
440
- ...(environmentId ? { environment: environmentId } : {}),
441
- steps: unwrapStepsAttachments(tr.steps),
442
- };
443
- const category = toUploadCategory(base, contextCategories ?? []);
444
- if (category) {
445
- base.category = category;
446
- }
447
- return base;
448
- }));
449
- }, _TestOpsPlugin_syncLaunchCategories = async function _TestOpsPlugin_syncLaunchCategories(trs, contextCategories) {
450
- const categoryNamesByExternalId = __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_collectCategoryNamesByExternalId).call(this, trs);
451
- if (categoryNamesByExternalId.size === 0) {
452
- return;
453
- }
454
- const bulkItems = [];
455
- const seenExternalIds = new Set();
456
- for (const tr of trs) {
457
- const cat = tr.category;
458
- if (!cat?.externalId)
459
- continue;
460
- if (seenExternalIds.has(cat.externalId))
461
- continue;
462
- seenExternalIds.add(cat.externalId);
463
- bulkItems.push({
464
- externalId: cat.externalId,
465
- name: categoryNamesByExternalId.get(cat.externalId) ?? categoryDisplayName(cat),
466
- hide: cat.hide,
467
- expand: cat.expand,
468
- });
469
- }
470
- const rankByExternalId = new Map();
471
- for (const c of contextCategories) {
472
- if (!rankByExternalId.has(c.id)) {
473
- rankByExternalId.set(c.id, c.index);
474
- }
475
- if (!rankByExternalId.has(c.name)) {
476
- rankByExternalId.set(c.name, c.index);
477
- }
478
- }
479
- const ranked = bulkItems.map((item, i) => ({
480
- item,
481
- i,
482
- rank: rankByExternalId.get(item.externalId),
483
- }));
484
- ranked.sort((a, b) => {
485
- const ar = a.rank ?? Number.POSITIVE_INFINITY;
486
- const br = b.rank ?? Number.POSITIVE_INFINITY;
487
- if (ar !== br)
488
- return ar - br;
489
- return a.i - b.i;
490
- });
491
- const orderedBulkItems = ranked.map((r) => r.item);
492
- const launchId = __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").launchId;
493
- try {
494
- const created = await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunchCategoriesBulk(launchId, orderedBulkItems);
495
- const categoryIdByExternalId = new Map(created.map((r) => [r.externalId, r.id]));
496
- __classPrivateFieldGet(this, _TestOpsPlugin_instances, "m", _TestOpsPlugin_assignCreatedCategoryIds).call(this, trs, categoryIdByExternalId);
497
- }
498
- catch {
499
- }
500
- }, _TestOpsPlugin_collectCategoryNamesByExternalId = function _TestOpsPlugin_collectCategoryNamesByExternalId(trs) {
501
- const map = new Map();
502
- for (const tr of trs) {
503
- const cat = tr.category;
504
- if (cat?.externalId) {
505
- map.set(cat.externalId, categoryDisplayName(cat));
506
- }
507
- }
508
- return map;
509
- }, _TestOpsPlugin_assignCreatedCategoryIds = function _TestOpsPlugin_assignCreatedCategoryIds(trs, idByExternalId) {
510
- for (const tr of trs) {
511
- const cat = tr.category;
512
- if (!cat?.externalId) {
513
- continue;
514
- }
515
- const id = idByExternalId.get(cat.externalId);
516
- if (typeof id === "number") {
517
- tr.category = { ...cat, id };
518
- }
519
- }
520
527
  }, _TestOpsPlugin_startUpload = async function _TestOpsPlugin_startUpload() {
521
- const launchGitContext = __classPrivateFieldGet(this, _TestOpsPlugin_gitFlow, "f").resolve();
528
+ const jobRunId = resolveJobRunIdFromEnv();
522
529
  try {
523
- await __classPrivateFieldGet(this, _TestOpsPlugin_client, "f").createLaunch(__classPrivateFieldGet(this, _TestOpsPlugin_launchName, "f"), __classPrivateFieldGet(this, _TestOpsPlugin_launchTags, "f"), launchGitContext);
524
- 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);
525
540
  __classPrivateFieldSet(this, _TestOpsPlugin_launchStarted, true, "f");
526
541
  }
527
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
+ };