@allurereport/plugin-testops 3.16.0 → 3.17.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/dist/errors.js ADDED
@@ -0,0 +1,134 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { KnownError, UnknownError } from "@allurereport/service";
3
+ import { isAxiosError } from "axios";
4
+ import { retryAfterMs } from "./utils/httpRetry.js";
5
+ export var ErrorKind;
6
+ (function (ErrorKind) {
7
+ ErrorKind["None"] = "none";
8
+ ErrorKind["ServiceTransient"] = "service_transient";
9
+ ErrorKind["ResourceRecoverable"] = "resource_recoverable";
10
+ ErrorKind["AuthTerminal"] = "auth_terminal";
11
+ ErrorKind["NotFoundTerminal"] = "not_found_terminal";
12
+ ErrorKind["PayloadTerminal"] = "payload_terminal";
13
+ ErrorKind["ValidationTerminal"] = "validation_terminal";
14
+ ErrorKind["ConflictTerminal"] = "conflict_terminal";
15
+ ErrorKind["Unknown"] = "unknown";
16
+ })(ErrorKind || (ErrorKind = {}));
17
+ const isLaunchClosedMessage = (message) => {
18
+ if (!message) {
19
+ return false;
20
+ }
21
+ const lower = message.toLowerCase();
22
+ return lower.includes("launch is closed") || lower.includes("closed launch");
23
+ };
24
+ const responseStatus = (error) => isAxiosError(error) ? error.response?.status : undefined;
25
+ const responseMessage = (error) => {
26
+ if (!isAxiosError(error)) {
27
+ return undefined;
28
+ }
29
+ const data = error.response?.data;
30
+ return data?.message;
31
+ };
32
+ const classifyHttpStatus = (status) => {
33
+ if (status === 408 || status === 429 || status >= 500) {
34
+ return ErrorKind.ServiceTransient;
35
+ }
36
+ if (status === 423) {
37
+ return ErrorKind.ResourceRecoverable;
38
+ }
39
+ if (status === 401 || status === 403) {
40
+ return ErrorKind.AuthTerminal;
41
+ }
42
+ if (status === 404) {
43
+ return ErrorKind.NotFoundTerminal;
44
+ }
45
+ if (status === 413 || status === 415) {
46
+ return ErrorKind.PayloadTerminal;
47
+ }
48
+ if (status === 409) {
49
+ return ErrorKind.ConflictTerminal;
50
+ }
51
+ if (status >= 400 && status < 500) {
52
+ return ErrorKind.ValidationTerminal;
53
+ }
54
+ return ErrorKind.Unknown;
55
+ };
56
+ export const isClosedLaunchError = (error) => {
57
+ if (isAxiosError(error)) {
58
+ return isLaunchClosedMessage(responseMessage(error));
59
+ }
60
+ if (error instanceof KnownError || error instanceof UnknownError) {
61
+ return isLaunchClosedMessage(error.message);
62
+ }
63
+ return false;
64
+ };
65
+ const isCanceledError = (error) => isAxiosError(error) && (error.code === "ERR_CANCELED" || error.name === "CanceledError");
66
+ export const classifyError = (error) => {
67
+ if (!error) {
68
+ return ErrorKind.None;
69
+ }
70
+ if (isCanceledError(error)) {
71
+ return ErrorKind.Unknown;
72
+ }
73
+ if (isAxiosError(error)) {
74
+ if (isLaunchClosedMessage(responseMessage(error))) {
75
+ return ErrorKind.ResourceRecoverable;
76
+ }
77
+ const status = responseStatus(error);
78
+ return status === undefined ? ErrorKind.ServiceTransient : classifyHttpStatus(status);
79
+ }
80
+ if (error instanceof KnownError) {
81
+ if (isLaunchClosedMessage(error.message)) {
82
+ return ErrorKind.ResourceRecoverable;
83
+ }
84
+ return typeof error.status === "number" ? classifyHttpStatus(error.status) : ErrorKind.Unknown;
85
+ }
86
+ if (error instanceof UnknownError) {
87
+ if (isLaunchClosedMessage(error.message)) {
88
+ return ErrorKind.ResourceRecoverable;
89
+ }
90
+ return ErrorKind.ServiceTransient;
91
+ }
92
+ if (error instanceof Error && (error.name === "AbortError" || /timeout/i.test(error.message))) {
93
+ return ErrorKind.ServiceTransient;
94
+ }
95
+ return ErrorKind.Unknown;
96
+ };
97
+ export const shouldRetryUpload = (error) => {
98
+ const kind = classifyError(error);
99
+ return kind === ErrorKind.ServiceTransient || kind === ErrorKind.ResourceRecoverable;
100
+ };
101
+ export const isPermanentUploadError = (error) => {
102
+ switch (classifyError(error)) {
103
+ case ErrorKind.AuthTerminal:
104
+ case ErrorKind.NotFoundTerminal:
105
+ case ErrorKind.PayloadTerminal:
106
+ case ErrorKind.ValidationTerminal:
107
+ case ErrorKind.ConflictTerminal:
108
+ return true;
109
+ default:
110
+ return false;
111
+ }
112
+ };
113
+ export const isTerminalUploadError = (error, retries, maxRetries) => isPermanentUploadError(error) || retries >= maxRetries;
114
+ const DEFAULT_MAX_RETRIES = 3;
115
+ const DEFAULT_BASE_DELAY_MS = 500;
116
+ const DEFAULT_MAX_DELAY_MS = 5000;
117
+ export const withUploadRetry = async (operation, options = {}) => {
118
+ const { maxRetries = DEFAULT_MAX_RETRIES, baseDelayMs = DEFAULT_BASE_DELAY_MS, maxDelayMs = DEFAULT_MAX_DELAY_MS, onRetry, } = options;
119
+ let attempt = 0;
120
+ for (;;) {
121
+ try {
122
+ return await operation();
123
+ }
124
+ catch (error) {
125
+ if (!shouldRetryUpload(error) || attempt >= maxRetries) {
126
+ throw error;
127
+ }
128
+ attempt += 1;
129
+ await onRetry?.(error, attempt);
130
+ const backoffMs = retryAfterMs(error) ?? Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
131
+ await delay(backoffMs);
132
+ }
133
+ }
134
+ };
@@ -11,6 +11,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
11
11
  };
12
12
  var _LaunchGitFlow_instances, _LaunchGitFlow_ci, _LaunchGitFlow_gitFlow, _LaunchGitFlow_ancestorLimit, _LaunchGitFlow_logger, _LaunchGitFlow_classify;
13
13
  import { collectGitFacts, isGitAvailable } from "@allurereport/git";
14
+ import { applyGitFactsOverrides } from "../utils/ciOverrides.js";
14
15
  import { buildGitFlowContext, shouldAttachGitFlow } from "./context.js";
15
16
  import { projectLaunchGitContext } from "./projection.js";
16
17
  export class LaunchGitFlow {
@@ -29,11 +30,18 @@ export class LaunchGitFlow {
29
30
  if (!shouldAttachGitFlow(__classPrivateFieldGet(this, _LaunchGitFlow_gitFlow, "f"))) {
30
31
  return undefined;
31
32
  }
32
- if (!isGitAvailable()) {
33
- __classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("git CLI is not available; continuing upload without git context");
33
+ const gitAvailable = isGitAvailable();
34
+ const collected = gitAvailable ? collectGitFacts({ ancestorLimit: __classPrivateFieldGet(this, _LaunchGitFlow_ancestorLimit, "f") }) : undefined;
35
+ const facts = applyGitFactsOverrides(collected);
36
+ if (!facts?.commit) {
37
+ if (!gitAvailable) {
38
+ __classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("git CLI is not available; continuing upload without git context");
39
+ }
40
+ else {
41
+ __classPrivateFieldGet(this, _LaunchGitFlow_logger, "f").warn("Git Flow metadata could not be collected; continuing upload without git context");
42
+ }
34
43
  return undefined;
35
44
  }
36
- const facts = collectGitFacts({ ancestorLimit: __classPrivateFieldGet(this, _LaunchGitFlow_ancestorLimit, "f") });
37
45
  const gitFlowContext = buildGitFlowContext({
38
46
  ci: __classPrivateFieldGet(this, _LaunchGitFlow_ci, "f"),
39
47
  facts,
@@ -1,3 +1,4 @@
1
1
  import type { TestOpsPluginOptions } from "../model.js";
2
2
  import type { ResolvedGitFlowOptions } from "./types.js";
3
+ export declare const parseEnvBool: (value?: string) => boolean | undefined;
3
4
  export declare const resolveGitFlowOptions: (options: TestOpsPluginOptions) => ResolvedGitFlowOptions;
@@ -1,6 +1,6 @@
1
1
  import { env } from "node:process";
2
2
  import { DEFAULT_ANCESTOR_LIMIT } from "@allurereport/git";
3
- const parseEnvBool = (value) => {
3
+ export const parseEnvBool = (value) => {
4
4
  if (value === undefined || value === "") {
5
5
  return undefined;
6
6
  }
package/dist/model.d.ts CHANGED
@@ -107,7 +107,6 @@ export type UploadTestResultDto = {
107
107
  trace?: string;
108
108
  hostId?: string;
109
109
  threadId?: string;
110
- environment?: string;
111
110
  category?: UploadTestResultCategoryDto;
112
111
  namedEnv?: UploadTestResultNamedEnvDto;
113
112
  steps?: UploadTestResultStepDto[];
@@ -173,16 +172,24 @@ export type LaunchCategoryBulkResult = {
173
172
  id: number;
174
173
  externalId: string;
175
174
  };
175
+ export type UploadRateLimit = {
176
+ windowMs: number;
177
+ maxRequestsPerWindow?: number;
178
+ maxFilesPerWindow?: number;
179
+ maxBytesPerWindow?: number;
180
+ };
176
181
  export type TestOpsClientParams = {
177
182
  baseUrl: string;
178
183
  projectId: string;
179
184
  accessToken: string;
180
185
  limit?: number;
186
+ uploadRateLimit?: UploadRateLimit | false;
181
187
  };
182
188
  export type AttachmentForUpload = {
183
189
  originalFileName: string;
184
190
  contentType: string;
185
191
  content: Buffer | Blob | ReadableStream | Readable;
192
+ contentLength?: number;
186
193
  };
187
194
  export type FixtureResolver = (tr: TestResult) => Promise<TestOpsFixtureResult[]>;
188
195
  export type AttachmentsResolver = (tr: TestResult) => Promise<AttachmentForUpload[] | AttachmentForUpload>;
@@ -193,10 +200,13 @@ export type TestOpsUploaderOptions = {
193
200
  launchName: string;
194
201
  launchTags: string[];
195
202
  autocloseLaunch?: boolean;
203
+ reopenClosedLaunch?: boolean;
204
+ launchId?: number;
196
205
  gitFlow?: boolean;
197
206
  ancestorLimit?: number;
198
207
  filter?: (testResult: TestResult) => boolean;
199
208
  limit?: number;
209
+ uploadRateLimit?: UploadRateLimit | false;
200
210
  };
201
211
  export interface TestOpsFixtureResult extends Omit<TestFixtureResult, "type"> {
202
212
  type: "BEFORE" | "AFTER";
@@ -210,6 +220,21 @@ export type TestOpsSession = {
210
220
  launchId: number;
211
221
  projectId: number;
212
222
  };
223
+ export type ExternalRunStartResponse = {
224
+ projectId: number;
225
+ launchId: number;
226
+ jobId?: number;
227
+ jobRunId?: number;
228
+ };
229
+ export type TestOpsJobParameter = {
230
+ name: string;
231
+ defaultValue: string;
232
+ };
233
+ export type TestOpsJob = {
234
+ id: number;
235
+ externalId: string;
236
+ parameters: TestOpsJobParameter[];
237
+ };
213
238
  export type TestOpsLaunch = {
214
239
  id: number;
215
240
  name: string;