@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/README.md +2 -0
- package/dist/client.d.ts +7 -3
- package/dist/client.js +169 -147
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +134 -0
- package/dist/gitFlow/options.d.ts +1 -0
- package/dist/gitFlow/options.js +1 -1
- package/dist/model.d.ts +26 -1
- package/dist/plugin.js +127 -141
- package/dist/uploadPacer.d.ts +12 -0
- package/dist/uploadPacer.js +76 -0
- package/dist/uploadQueue.d.ts +15 -0
- package/dist/uploadQueue.js +61 -0
- package/dist/utils/ciOverrides.d.ts +2 -0
- package/dist/utils/ciOverrides.js +20 -0
- package/dist/utils/httpRetry.d.ts +4 -0
- package/dist/utils/httpRetry.js +62 -0
- package/dist/utils/jobParameters.d.ts +15 -0
- package/dist/utils/jobParameters.js +39 -0
- package/dist/utils/launchCategories.d.ts +7 -0
- package/dist/utils/launchCategories.js +97 -0
- package/dist/utils/options.d.ts +1 -0
- package/dist/utils/options.js +14 -2
- package/dist/utils/resolvers.js +1 -0
- package/dist/utils/testResults.js +0 -1
- package/package.json +8 -8
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
const OVERRIDABLE_FIELDS = [
|
|
3
|
+
["jobUid", "ALLURE_JOB_UID"],
|
|
4
|
+
["jobUrl", "ALLURE_JOB_URL"],
|
|
5
|
+
["jobName", "ALLURE_JOB_NAME"],
|
|
6
|
+
["jobRunUid", "ALLURE_JOB_RUN_UID"],
|
|
7
|
+
["jobRunUrl", "ALLURE_JOB_RUN_URL"],
|
|
8
|
+
["jobRunName", "ALLURE_JOB_RUN_NAME"],
|
|
9
|
+
["jobRunBranch", "ALLURE_JOB_RUN_BRANCH"],
|
|
10
|
+
];
|
|
11
|
+
export const applyCiOverrides = (ci) => {
|
|
12
|
+
const overrides = {};
|
|
13
|
+
for (const [field, envVar] of OVERRIDABLE_FIELDS) {
|
|
14
|
+
const value = env[envVar];
|
|
15
|
+
if (value) {
|
|
16
|
+
overrides[field] = value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(overrides).length > 0 ? { ...ci, ...overrides } : ci;
|
|
20
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AttachmentForUpload } from "../model.js";
|
|
2
|
+
export declare const retryAfterMs: (error: unknown) => number | undefined;
|
|
3
|
+
export declare const retryRequest: <T>(request: () => Promise<T>) => Promise<T>;
|
|
4
|
+
export declare const attachmentByteLength: (attachment: Pick<AttachmentForUpload, "content" | "contentLength">) => number;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isAxiosError } from "axios";
|
|
2
|
+
const MAX_REQUEST_RETRIES = 3;
|
|
3
|
+
const RETRY_BASE_DELAY_MS = 500;
|
|
4
|
+
const MAX_RETRY_AFTER_DELAY_MS = 60000;
|
|
5
|
+
const getAxiosCause = (error) => {
|
|
6
|
+
if (isAxiosError(error)) {
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
return error instanceof Error && isAxiosError(error.cause) ? error.cause : undefined;
|
|
10
|
+
};
|
|
11
|
+
const isRetryableRequestError = (error) => {
|
|
12
|
+
const cause = getAxiosCause(error);
|
|
13
|
+
if (!cause || cause.code === "ERR_CANCELED" || cause.name === "CanceledError") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const status = cause.response?.status;
|
|
17
|
+
return status === undefined || status === 408 || status === 429 || status >= 500;
|
|
18
|
+
};
|
|
19
|
+
const parseRetryAfterMs = (error) => {
|
|
20
|
+
const headers = getAxiosCause(error)?.response?.headers;
|
|
21
|
+
const value = typeof headers?.get === "function" ? headers.get("retry-after") : headers?.["retry-after"];
|
|
22
|
+
const normalized = Array.isArray(value) ? value[0] : value;
|
|
23
|
+
if (typeof normalized !== "string" && typeof normalized !== "number") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const seconds = Number(normalized);
|
|
27
|
+
if (Number.isFinite(seconds)) {
|
|
28
|
+
return Math.max(0, seconds * 1000);
|
|
29
|
+
}
|
|
30
|
+
const date = Date.parse(String(normalized));
|
|
31
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
32
|
+
};
|
|
33
|
+
export const retryAfterMs = (error) => {
|
|
34
|
+
const requestedDelayMs = parseRetryAfterMs(error);
|
|
35
|
+
return requestedDelayMs === undefined ? undefined : Math.min(requestedDelayMs, MAX_RETRY_AFTER_DELAY_MS);
|
|
36
|
+
};
|
|
37
|
+
export const retryRequest = async (request) => {
|
|
38
|
+
for (let attempt = 0;; attempt += 1) {
|
|
39
|
+
try {
|
|
40
|
+
return await request();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (attempt >= MAX_REQUEST_RETRIES || !isRetryableRequestError(error)) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, retryAfterMs(error) ?? RETRY_BASE_DELAY_MS * 2 ** attempt));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const attachmentByteLength = (attachment) => {
|
|
51
|
+
const { content, contentLength } = attachment;
|
|
52
|
+
if (typeof contentLength === "number") {
|
|
53
|
+
return contentLength;
|
|
54
|
+
}
|
|
55
|
+
if (Buffer.isBuffer(content)) {
|
|
56
|
+
return content.length;
|
|
57
|
+
}
|
|
58
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) {
|
|
59
|
+
return content.size;
|
|
60
|
+
}
|
|
61
|
+
return 0;
|
|
62
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CiType } from "@allurereport/core-api";
|
|
2
|
+
import type { CiDescriptor } from "@allurereport/core-api";
|
|
3
|
+
import type { TestOpsJobParameter } from "../model.js";
|
|
4
|
+
export declare const TESTOPS_CI_TYPE: Partial<Record<CiType, string>>;
|
|
5
|
+
export declare const ciEndpoint: (ci: CiDescriptor) => string | undefined;
|
|
6
|
+
export declare const externalParameters: (ci: CiDescriptor, existingJobParameters?: TestOpsJobParameter[]) => {
|
|
7
|
+
job: {
|
|
8
|
+
name: string;
|
|
9
|
+
defaultValue: string;
|
|
10
|
+
}[];
|
|
11
|
+
jobRun: {
|
|
12
|
+
name: string;
|
|
13
|
+
value: string;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { env } from "node:process";
|
|
2
|
+
import { CiType } from "@allurereport/core-api";
|
|
3
|
+
export const TESTOPS_CI_TYPE = {
|
|
4
|
+
[CiType.Amazon]: "aws",
|
|
5
|
+
[CiType.Circle]: "circleci",
|
|
6
|
+
};
|
|
7
|
+
export const ciEndpoint = (ci) => {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(ci.jobUrl).origin;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const externalParameters = (ci, existingJobParameters = []) => {
|
|
16
|
+
const jobParams = {};
|
|
17
|
+
const jobRunParams = {};
|
|
18
|
+
for (const { name, defaultValue } of existingJobParameters) {
|
|
19
|
+
jobParams[name] = defaultValue;
|
|
20
|
+
if (env[name]) {
|
|
21
|
+
jobRunParams[name] = env[name];
|
|
22
|
+
}
|
|
23
|
+
else if (ci.type === CiType.Bitbucket && name === "CustomName" && defaultValue) {
|
|
24
|
+
jobRunParams[name] = defaultValue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (ci.jobRunBranch) {
|
|
28
|
+
jobParams.Branch = ci.jobRunBranch;
|
|
29
|
+
jobRunParams.Branch = ci.jobRunBranch;
|
|
30
|
+
}
|
|
31
|
+
if (ci.type === CiType.Bitbucket && env.CustomName) {
|
|
32
|
+
jobParams.CustomName = env.CustomName;
|
|
33
|
+
jobRunParams.CustomName = env.CustomName;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
job: Object.entries(jobParams).map(([name, defaultValue]) => ({ name, defaultValue })),
|
|
37
|
+
jobRun: Object.entries(jobRunParams).map(([name, value]) => ({ name, value })),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CategoryDefinition } from "@allurereport/core-api";
|
|
2
|
+
import type { AllureStore } from "@allurereport/plugin-api";
|
|
3
|
+
import type { TestOpsClient } from "../client.js";
|
|
4
|
+
import type { TestOpsPluginTestResult, UploadCategory } from "../model.js";
|
|
5
|
+
export declare const categoryDisplayName: (cat: UploadCategory) => string;
|
|
6
|
+
export declare const enrichWithCategories: (store: AllureStore, trs: TestOpsPluginTestResult[], contextCategories: CategoryDefinition[]) => Promise<TestOpsPluginTestResult[]>;
|
|
7
|
+
export declare const syncLaunchCategories: (client: TestOpsClient, trs: TestOpsPluginTestResult[], contextCategories: CategoryDefinition[]) => Promise<Error | undefined>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { toUploadCategory } from "./categories.js";
|
|
2
|
+
import { unwrapStepsAttachments } from "./resolvers.js";
|
|
3
|
+
export const categoryDisplayName = (cat) => cat.name ?? cat.grouping?.[0]?.name ?? cat.grouping?.[0]?.value ?? cat.grouping?.[0]?.key ?? cat.externalId;
|
|
4
|
+
export const enrichWithCategories = async (store, trs, contextCategories) => {
|
|
5
|
+
return Promise.all(trs.map(async (tr) => {
|
|
6
|
+
const environmentId = await store.environmentIdByTrId(tr.id);
|
|
7
|
+
const base = {
|
|
8
|
+
...tr,
|
|
9
|
+
...(environmentId ? { environment: environmentId } : {}),
|
|
10
|
+
steps: unwrapStepsAttachments(tr.steps),
|
|
11
|
+
};
|
|
12
|
+
const category = toUploadCategory(base, contextCategories ?? []);
|
|
13
|
+
if (category) {
|
|
14
|
+
base.category = category;
|
|
15
|
+
}
|
|
16
|
+
return base;
|
|
17
|
+
}));
|
|
18
|
+
};
|
|
19
|
+
const collectCategoryNamesByExternalId = (trs) => {
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const tr of trs) {
|
|
22
|
+
const cat = tr.category;
|
|
23
|
+
if (cat?.externalId) {
|
|
24
|
+
map.set(cat.externalId, categoryDisplayName(cat));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return map;
|
|
28
|
+
};
|
|
29
|
+
const assignCreatedCategoryIds = (trs, idByExternalId) => {
|
|
30
|
+
for (const tr of trs) {
|
|
31
|
+
const cat = tr.category;
|
|
32
|
+
if (!cat?.externalId) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const id = idByExternalId.get(cat.externalId);
|
|
36
|
+
if (typeof id === "number") {
|
|
37
|
+
tr.category = { ...cat, id };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export const syncLaunchCategories = async (client, trs, contextCategories) => {
|
|
42
|
+
const categoryNamesByExternalId = collectCategoryNamesByExternalId(trs);
|
|
43
|
+
if (categoryNamesByExternalId.size === 0) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const bulkItems = [];
|
|
47
|
+
const seenExternalIds = new Set();
|
|
48
|
+
for (const tr of trs) {
|
|
49
|
+
const cat = tr.category;
|
|
50
|
+
if (!cat?.externalId)
|
|
51
|
+
continue;
|
|
52
|
+
if (seenExternalIds.has(cat.externalId))
|
|
53
|
+
continue;
|
|
54
|
+
seenExternalIds.add(cat.externalId);
|
|
55
|
+
bulkItems.push({
|
|
56
|
+
externalId: cat.externalId,
|
|
57
|
+
name: categoryNamesByExternalId.get(cat.externalId) ?? categoryDisplayName(cat),
|
|
58
|
+
hide: cat.hide,
|
|
59
|
+
expand: cat.expand,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const rankByExternalId = new Map();
|
|
63
|
+
for (const category of contextCategories) {
|
|
64
|
+
if (!rankByExternalId.has(category.id)) {
|
|
65
|
+
rankByExternalId.set(category.id, category.index);
|
|
66
|
+
}
|
|
67
|
+
if (!rankByExternalId.has(category.name)) {
|
|
68
|
+
rankByExternalId.set(category.name, category.index);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const ranked = bulkItems.map((item, originalIndex) => ({
|
|
72
|
+
item,
|
|
73
|
+
originalIndex,
|
|
74
|
+
rank: rankByExternalId.get(item.externalId),
|
|
75
|
+
}));
|
|
76
|
+
ranked.sort((first, second) => {
|
|
77
|
+
const firstRank = first.rank ?? Number.POSITIVE_INFINITY;
|
|
78
|
+
const secondRank = second.rank ?? Number.POSITIVE_INFINITY;
|
|
79
|
+
if (firstRank !== secondRank)
|
|
80
|
+
return firstRank - secondRank;
|
|
81
|
+
return first.originalIndex - second.originalIndex;
|
|
82
|
+
});
|
|
83
|
+
const orderedBulkItems = ranked.map((rankedItem) => rankedItem.item);
|
|
84
|
+
const launchId = client.launchId;
|
|
85
|
+
if (launchId === undefined) {
|
|
86
|
+
return new Error("Launch isn't created, skipping launch categories");
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const created = await client.createLaunchCategoriesBulk(launchId, orderedBulkItems);
|
|
90
|
+
const categoryIdByExternalId = new Map(created.map((result) => [result.externalId, result.id]));
|
|
91
|
+
assignCreatedCategoryIds(trs, categoryIdByExternalId);
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
96
|
+
}
|
|
97
|
+
};
|
package/dist/utils/options.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { TestOpsPluginOptions } from "../model.js";
|
|
2
|
+
export declare const toPositiveInteger: (value: string | number | undefined) => number | undefined;
|
|
2
3
|
export declare const resolvePluginOptions: (options: TestOpsPluginOptions) => Omit<TestOpsPluginOptions, "filter">;
|
package/dist/utils/options.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { env } from "node:process";
|
|
2
|
+
import { parseEnvBool } from "../gitFlow/options.js";
|
|
3
|
+
export const toPositiveInteger = (value) => {
|
|
4
|
+
if (value === undefined || value === "") {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
9
|
+
};
|
|
2
10
|
export const resolvePluginOptions = (options) => {
|
|
3
|
-
const { ALLURE_TOKEN, ALLURE_ENDPOINT, ALLURE_PROJECT_ID, ALLURE_LAUNCH_TAGS, ALLURE_LAUNCH_NAME } = env;
|
|
4
|
-
const { accessToken = ALLURE_TOKEN, endpoint = ALLURE_ENDPOINT, projectId = ALLURE_PROJECT_ID, launchTags = ALLURE_LAUNCH_TAGS, launchName = ALLURE_LAUNCH_NAME, autocloseLaunch, } = options;
|
|
11
|
+
const { ALLURE_TOKEN, ALLURE_ENDPOINT, ALLURE_PROJECT_ID, ALLURE_LAUNCH_TAGS, ALLURE_LAUNCH_NAME, ALLURE_LAUNCH_ID } = env;
|
|
12
|
+
const { accessToken = ALLURE_TOKEN, endpoint = ALLURE_ENDPOINT, projectId = ALLURE_PROJECT_ID, launchTags = ALLURE_LAUNCH_TAGS, launchName = ALLURE_LAUNCH_NAME, launchId = toPositiveInteger(ALLURE_LAUNCH_ID), autocloseLaunch, uploadRateLimit, reopenClosedLaunch = parseEnvBool(env.ALLURE_REOPEN_CLOSED_LAUNCH) ?? false, } = options;
|
|
5
13
|
const tags = !launchTags
|
|
6
14
|
? []
|
|
7
15
|
: Array.isArray(launchTags)
|
|
8
16
|
? launchTags
|
|
9
17
|
: launchTags.split(",").map((tag) => tag.trim());
|
|
18
|
+
const resolvedLaunchId = toPositiveInteger(launchId);
|
|
10
19
|
return {
|
|
11
20
|
launchName: launchName || "Allure Report",
|
|
12
21
|
launchTags: tags,
|
|
@@ -14,5 +23,8 @@ export const resolvePluginOptions = (options) => {
|
|
|
14
23
|
endpoint: endpoint || "",
|
|
15
24
|
projectId: projectId || "",
|
|
16
25
|
...(autocloseLaunch !== undefined ? { autocloseLaunch } : {}),
|
|
26
|
+
...(uploadRateLimit !== undefined ? { uploadRateLimit } : {}),
|
|
27
|
+
...(resolvedLaunchId !== undefined ? { launchId: resolvedLaunchId } : {}),
|
|
28
|
+
reopenClosedLaunch,
|
|
17
29
|
};
|
|
18
30
|
};
|
package/dist/utils/resolvers.js
CHANGED
|
@@ -89,7 +89,6 @@ export const toUploadTestResultDto = (tr) => {
|
|
|
89
89
|
trace: tr.trace,
|
|
90
90
|
hostId: tr.hostId,
|
|
91
91
|
threadId: tr.threadId,
|
|
92
|
-
environment: tr.environment,
|
|
93
92
|
...(category ? { category } : {}),
|
|
94
93
|
...(typeof tr.namedEnv?.id === "number" ? { namedEnv: { id: tr.namedEnv.id } } : {}),
|
|
95
94
|
steps: tr.steps?.map(toUploadStepDto),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/plugin-testops",
|
|
3
|
-
"version": "3.16.
|
|
3
|
+
"version": "3.16.1",
|
|
4
4
|
"description": "Allure Plugin TestOps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
"lint:fix": "oxlint --import-plugin --fix src test features stories"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@allurereport/ci": "3.16.
|
|
36
|
-
"@allurereport/cli-commons": "3.16.
|
|
37
|
-
"@allurereport/core-api": "3.16.
|
|
38
|
-
"@allurereport/git": "3.16.
|
|
39
|
-
"@allurereport/plugin-api": "3.16.
|
|
40
|
-
"@allurereport/reader-api": "3.16.
|
|
41
|
-
"@allurereport/service": "3.16.
|
|
35
|
+
"@allurereport/ci": "3.16.1",
|
|
36
|
+
"@allurereport/cli-commons": "3.16.1",
|
|
37
|
+
"@allurereport/core-api": "3.16.1",
|
|
38
|
+
"@allurereport/git": "3.16.1",
|
|
39
|
+
"@allurereport/plugin-api": "3.16.1",
|
|
40
|
+
"@allurereport/reader-api": "3.16.1",
|
|
41
|
+
"@allurereport/service": "3.16.1",
|
|
42
42
|
"axios": "^1.18.1",
|
|
43
43
|
"form-data": "^4.0.6",
|
|
44
44
|
"lodash-es": "^4.18.1",
|