@allurereport/ci 3.18.0 → 3.19.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/detectors/gitlab.d.ts +15 -1
- package/dist/detectors/gitlab.js +51 -11
- package/dist/helpers/gitlab/client.d.ts +14 -0
- package/dist/helpers/gitlab/client.js +133 -0
- package/dist/helpers/gitlab/comments.d.ts +6 -0
- package/dist/helpers/gitlab/comments.js +128 -0
- package/dist/helpers/gitlab/history.d.ts +4 -0
- package/dist/helpers/gitlab/history.js +111 -0
- package/dist/helpers/gitlab/index.d.ts +4 -0
- package/dist/helpers/gitlab/index.js +3 -0
- package/dist/helpers/gitlab/summary.d.ts +2 -0
- package/dist/helpers/gitlab/summary.js +19 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/reportContext.js +39 -5
- package/dist/reportMarkdown.d.ts +11 -1
- package/dist/reportMarkdown.js +51 -21
- package/package.json +5 -4
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
import { type CiDescriptor } from "@allurereport/core-api";
|
|
2
|
-
export
|
|
2
|
+
export type GitlabCiDescriptor = CiDescriptor & {
|
|
3
|
+
projectId: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
projectDirectory: string;
|
|
6
|
+
pipelineSource: string;
|
|
7
|
+
ciJobName: string;
|
|
8
|
+
currentJobId: string;
|
|
9
|
+
currentJobUrl: string;
|
|
10
|
+
mergeRequestProjectId: string;
|
|
11
|
+
restApiUrl: string;
|
|
12
|
+
graphqlApiUrl: string;
|
|
13
|
+
ref: string;
|
|
14
|
+
jobArtifactsUrlBase: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const gitlab: GitlabCiDescriptor;
|
package/dist/detectors/gitlab.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { CiType, GitProvider } from "@allurereport/core-api";
|
|
2
2
|
import { getEnv } from "../utils.js";
|
|
3
|
+
const GITLAB_DEFAULT_SERVER_URL = "https://gitlab.com";
|
|
4
|
+
const stripTrailingSlashes = (value) => value.replace(/\/+$/, "");
|
|
3
5
|
const getMergeRequestProjectUrl = () => getEnv("CI_MERGE_REQUEST_PROJECT_URL") || getEnv("CI_PROJECT_URL");
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const getSourceRef = () => getEnv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME") || getEnv("CI_COMMIT_REF_NAME") || undefined;
|
|
7
|
+
const getServerUrl = () => stripTrailingSlashes(getEnv("CI_SERVER_URL") || GITLAB_DEFAULT_SERVER_URL);
|
|
8
|
+
const getRestApiUrl = () => stripTrailingSlashes(getEnv("CI_API_V4_URL") || `${getServerUrl()}/api/v4`);
|
|
9
|
+
const getGraphqlApiUrl = () => {
|
|
10
|
+
const explicit = getEnv("CI_API_GRAPHQL_URL");
|
|
11
|
+
if (explicit) {
|
|
12
|
+
return stripTrailingSlashes(explicit);
|
|
8
13
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
getEnv("CI_COMMIT_REF_NAME") ||
|
|
12
|
-
undefined);
|
|
14
|
+
const restApiUrl = getRestApiUrl();
|
|
15
|
+
return restApiUrl.replace(/\/api\/v4$/, "/api/graphql");
|
|
13
16
|
};
|
|
14
17
|
export const gitlab = {
|
|
15
18
|
type: CiType.Gitlab,
|
|
@@ -28,6 +31,9 @@ export const gitlab = {
|
|
|
28
31
|
get jobName() {
|
|
29
32
|
return getEnv("CI_PROJECT_NAME");
|
|
30
33
|
},
|
|
34
|
+
get ciJobName() {
|
|
35
|
+
return getEnv("CI_JOB_NAME");
|
|
36
|
+
},
|
|
31
37
|
get jobRunUid() {
|
|
32
38
|
return getEnv("CI_PIPELINE_ID");
|
|
33
39
|
},
|
|
@@ -38,14 +44,14 @@ export const gitlab = {
|
|
|
38
44
|
return getEnv("CI_PIPELINE_ID");
|
|
39
45
|
},
|
|
40
46
|
get jobRunBranch() {
|
|
41
|
-
return
|
|
47
|
+
return getSourceRef() || "";
|
|
42
48
|
},
|
|
43
49
|
get pullRequestUrl() {
|
|
44
50
|
const mergeRequestIID = getEnv("CI_MERGE_REQUEST_IID");
|
|
45
51
|
if (!mergeRequestIID) {
|
|
46
52
|
return "";
|
|
47
53
|
}
|
|
48
|
-
const projectUrl = getMergeRequestProjectUrl()
|
|
54
|
+
const projectUrl = stripTrailingSlashes(getMergeRequestProjectUrl());
|
|
49
55
|
return projectUrl ? `${projectUrl}/-/merge_requests/${mergeRequestIID}` : "";
|
|
50
56
|
},
|
|
51
57
|
get pullRequestName() {
|
|
@@ -64,7 +70,7 @@ export const gitlab = {
|
|
|
64
70
|
: undefined;
|
|
65
71
|
},
|
|
66
72
|
get sourceBranch() {
|
|
67
|
-
return
|
|
73
|
+
return getSourceRef();
|
|
68
74
|
},
|
|
69
75
|
get targetBranch() {
|
|
70
76
|
return getEnv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME") || undefined;
|
|
@@ -79,4 +85,38 @@ export const gitlab = {
|
|
|
79
85
|
}
|
|
80
86
|
: undefined;
|
|
81
87
|
},
|
|
88
|
+
get projectId() {
|
|
89
|
+
return getEnv("CI_PROJECT_ID");
|
|
90
|
+
},
|
|
91
|
+
get projectPath() {
|
|
92
|
+
return getEnv("CI_PROJECT_PATH");
|
|
93
|
+
},
|
|
94
|
+
get projectDirectory() {
|
|
95
|
+
return getEnv("CI_PROJECT_DIR");
|
|
96
|
+
},
|
|
97
|
+
get pipelineSource() {
|
|
98
|
+
return getEnv("CI_PIPELINE_SOURCE");
|
|
99
|
+
},
|
|
100
|
+
get currentJobId() {
|
|
101
|
+
return getEnv("CI_JOB_ID");
|
|
102
|
+
},
|
|
103
|
+
get currentJobUrl() {
|
|
104
|
+
return getEnv("CI_JOB_URL");
|
|
105
|
+
},
|
|
106
|
+
get mergeRequestProjectId() {
|
|
107
|
+
return getEnv("CI_MERGE_REQUEST_PROJECT_ID");
|
|
108
|
+
},
|
|
109
|
+
get restApiUrl() {
|
|
110
|
+
return getRestApiUrl();
|
|
111
|
+
},
|
|
112
|
+
get graphqlApiUrl() {
|
|
113
|
+
return getGraphqlApiUrl();
|
|
114
|
+
},
|
|
115
|
+
get ref() {
|
|
116
|
+
return getSourceRef() || "";
|
|
117
|
+
},
|
|
118
|
+
get jobArtifactsUrlBase() {
|
|
119
|
+
return (`${new URL(getServerUrl()).protocol}//${getEnv("CI_PROJECT_ROOT_NAMESPACE_SLUG")}.${getEnv("CI_PAGES_DOMAIN") || "gitlab.io"}` +
|
|
120
|
+
`/-/${getEnv("CI_PROJECT_NAME")}/-/jobs/${this.currentJobId}/artifacts`);
|
|
121
|
+
},
|
|
82
122
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type AxiosResponse } from "axios";
|
|
2
|
+
import { type GitlabCiDescriptor } from "../../detectors/gitlab.js";
|
|
3
|
+
export type GitlabClient = {
|
|
4
|
+
ci: GitlabCiDescriptor;
|
|
5
|
+
query<T>(query: string, variables: Record<string, unknown>): Promise<T>;
|
|
6
|
+
requestJson<T>(method: "GET" | "POST" | "PUT", path: string, body?: unknown): Promise<{
|
|
7
|
+
data: T;
|
|
8
|
+
headers: AxiosResponse["headers"];
|
|
9
|
+
}>;
|
|
10
|
+
downloadArtifact(jobId: string, artifactPath: string): Promise<Uint8Array>;
|
|
11
|
+
};
|
|
12
|
+
export declare const createGitlabClient: (options?: {
|
|
13
|
+
token?: string;
|
|
14
|
+
}) => GitlabClient;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { gitlab } from "../../detectors/gitlab.js";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 10000;
|
|
4
|
+
const isDecimalString = (value) => /^[0-9]+$/.test(value);
|
|
5
|
+
const requireMetadata = (ci) => {
|
|
6
|
+
const missing = [];
|
|
7
|
+
if (!ci.detected) {
|
|
8
|
+
missing.push("GitLab CI");
|
|
9
|
+
}
|
|
10
|
+
if (!ci.projectId) {
|
|
11
|
+
missing.push("CI_PROJECT_ID");
|
|
12
|
+
}
|
|
13
|
+
if (!ci.projectPath) {
|
|
14
|
+
missing.push("CI_PROJECT_PATH");
|
|
15
|
+
}
|
|
16
|
+
if (!ci.projectDirectory) {
|
|
17
|
+
missing.push("CI_PROJECT_DIR");
|
|
18
|
+
}
|
|
19
|
+
if (!ci.jobRunUid) {
|
|
20
|
+
missing.push("CI_PIPELINE_ID");
|
|
21
|
+
}
|
|
22
|
+
if (!ci.pipelineSource) {
|
|
23
|
+
missing.push("CI_PIPELINE_SOURCE");
|
|
24
|
+
}
|
|
25
|
+
if (!ci.ciJobName) {
|
|
26
|
+
missing.push("CI_JOB_NAME");
|
|
27
|
+
}
|
|
28
|
+
if (!ci.ref) {
|
|
29
|
+
missing.push("CI_COMMIT_REF_NAME");
|
|
30
|
+
}
|
|
31
|
+
return missing;
|
|
32
|
+
};
|
|
33
|
+
const invalidNumericMetadata = (ci) => {
|
|
34
|
+
const invalid = [];
|
|
35
|
+
if (!isDecimalString(ci.projectId)) {
|
|
36
|
+
invalid.push("CI_PROJECT_ID");
|
|
37
|
+
}
|
|
38
|
+
if (!isDecimalString(ci.jobRunUid)) {
|
|
39
|
+
invalid.push("CI_PIPELINE_ID");
|
|
40
|
+
}
|
|
41
|
+
if (ci.currentJobId && !isDecimalString(ci.currentJobId)) {
|
|
42
|
+
invalid.push("CI_JOB_ID");
|
|
43
|
+
}
|
|
44
|
+
if (ci.mergeRequestProjectId && !isDecimalString(ci.mergeRequestProjectId)) {
|
|
45
|
+
invalid.push("CI_MERGE_REQUEST_PROJECT_ID");
|
|
46
|
+
}
|
|
47
|
+
return invalid;
|
|
48
|
+
};
|
|
49
|
+
const encodeArtifactPathSegment = (segment) => {
|
|
50
|
+
if (segment === ".") {
|
|
51
|
+
return "%2E";
|
|
52
|
+
}
|
|
53
|
+
if (segment === "..") {
|
|
54
|
+
return "%2E%2E";
|
|
55
|
+
}
|
|
56
|
+
return encodeURIComponent(segment);
|
|
57
|
+
};
|
|
58
|
+
const encodeArtifactPath = (artifactPath) => artifactPath.split("/").map(encodeArtifactPathSegment).join("/");
|
|
59
|
+
const buildRestUrl = (restApiUrl, path) => new URL(path.replace(/^\/+/, ""), `${restApiUrl.href.replace(/\/+$/, "")}/`);
|
|
60
|
+
const hasApiPathSuffix = (url, suffix) => url.pathname.replace(/\/+$/, "").endsWith(suffix);
|
|
61
|
+
const assertTrustedApiUrl = (url, apiOrigin) => {
|
|
62
|
+
if (url.origin !== apiOrigin) {
|
|
63
|
+
throw new Error("GitLab API request left trusted origin");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
export const createGitlabClient = (options) => {
|
|
67
|
+
const token = options?.token;
|
|
68
|
+
const ci = gitlab;
|
|
69
|
+
const missingMetadata = requireMetadata(ci);
|
|
70
|
+
if (missingMetadata.length > 0) {
|
|
71
|
+
throw new Error(`missing GitLab metadata: ${missingMetadata.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
const invalidNumeric = invalidNumericMetadata(ci);
|
|
74
|
+
if (invalidNumeric.length > 0) {
|
|
75
|
+
throw new Error(`invalid numeric GitLab metadata: ${invalidNumeric.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
const restApiUrl = new URL(ci.restApiUrl);
|
|
78
|
+
const graphqlApiUrl = new URL(ci.graphqlApiUrl);
|
|
79
|
+
if (!hasApiPathSuffix(restApiUrl, "/api/v4") || !hasApiPathSuffix(graphqlApiUrl, "/api/graphql")) {
|
|
80
|
+
throw new Error("invalid GitLab API endpoint paths");
|
|
81
|
+
}
|
|
82
|
+
if (restApiUrl.origin !== graphqlApiUrl.origin) {
|
|
83
|
+
throw new Error("GitLab API origins do not match");
|
|
84
|
+
}
|
|
85
|
+
const apiOrigin = restApiUrl.origin;
|
|
86
|
+
const requestOptions = {
|
|
87
|
+
headers: token ? { "private-token": token } : {},
|
|
88
|
+
sensitiveHeaders: ["private-token"],
|
|
89
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
ci,
|
|
93
|
+
async query(query, variables) {
|
|
94
|
+
const { data: envelope } = await axios.request({
|
|
95
|
+
...requestOptions,
|
|
96
|
+
url: graphqlApiUrl.href,
|
|
97
|
+
method: "POST",
|
|
98
|
+
data: { query, variables },
|
|
99
|
+
});
|
|
100
|
+
if (!envelope || typeof envelope !== "object") {
|
|
101
|
+
throw new Error("GitLab GraphQL response was invalid");
|
|
102
|
+
}
|
|
103
|
+
if (envelope.errors) {
|
|
104
|
+
throw new Error("GitLab GraphQL response contained errors");
|
|
105
|
+
}
|
|
106
|
+
if (!Object.hasOwn(envelope, "data")) {
|
|
107
|
+
throw new Error("GitLab GraphQL response omitted data");
|
|
108
|
+
}
|
|
109
|
+
return envelope.data;
|
|
110
|
+
},
|
|
111
|
+
async requestJson(method, path, body) {
|
|
112
|
+
const url = buildRestUrl(restApiUrl, path);
|
|
113
|
+
assertTrustedApiUrl(url, apiOrigin);
|
|
114
|
+
const { data, headers } = await axios.request({
|
|
115
|
+
...requestOptions,
|
|
116
|
+
url: url.href,
|
|
117
|
+
method,
|
|
118
|
+
data: body,
|
|
119
|
+
});
|
|
120
|
+
return { data, headers };
|
|
121
|
+
},
|
|
122
|
+
async downloadArtifact(jobId, artifactPath) {
|
|
123
|
+
const url = buildRestUrl(restApiUrl, `/projects/${encodeURIComponent(ci.projectId)}/jobs/${encodeURIComponent(jobId)}/artifacts/${encodeArtifactPath(artifactPath)}`);
|
|
124
|
+
const { data } = await axios.request({
|
|
125
|
+
...requestOptions,
|
|
126
|
+
url: url.href,
|
|
127
|
+
method: "GET",
|
|
128
|
+
responseType: "arraybuffer",
|
|
129
|
+
});
|
|
130
|
+
return data;
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { gitlab } from "../../detectors/gitlab.js";
|
|
2
|
+
import { createGitlabClient } from "./client.js";
|
|
3
|
+
import { renderGitlabReportSummary } from "./summary.js";
|
|
4
|
+
const MARKER_VERSION = "v1";
|
|
5
|
+
const MAX_NOTE_SCAN_PAGES = 5;
|
|
6
|
+
const NOTE_PAGE_SIZE = 100;
|
|
7
|
+
const MAX_COMMENT_LENGTH = 60000;
|
|
8
|
+
const isDecimalString = (value) => /^[0-9]+$/.test(value);
|
|
9
|
+
const encodePathSegment = (value) => encodeURIComponent(value);
|
|
10
|
+
const ownershipMarker = ({ jobName, pipelineId, jobId }) => `<!-- allure-gitlab-summary:${MARKER_VERSION}:${jobName}:${pipelineId}:${jobId} -->`;
|
|
11
|
+
const parseNotes = (value) => {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
throw new Error("invalid notes response");
|
|
14
|
+
}
|
|
15
|
+
return value.map((note) => {
|
|
16
|
+
if (typeof note !== "object" || note === null) {
|
|
17
|
+
throw new Error("invalid notes response");
|
|
18
|
+
}
|
|
19
|
+
const { id, body } = note;
|
|
20
|
+
const textId = typeof id === "number" && Number.isInteger(id) ? String(id) : typeof id === "string" ? id : "";
|
|
21
|
+
if (!textId || typeof body !== "string") {
|
|
22
|
+
throw new Error("invalid notes response");
|
|
23
|
+
}
|
|
24
|
+
return { id: textId, body };
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const markerPattern = new RegExp(`^<!-- allure-gitlab-summary:${MARKER_VERSION}:([A-Za-z0-9+/]+={0,2}):([0-9]+):([0-9]+) -->\\n`);
|
|
28
|
+
const parseMarker = (body) => {
|
|
29
|
+
const match = body.match(markerPattern);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
jobName: match[1],
|
|
35
|
+
pipelineId: BigInt(match[2]),
|
|
36
|
+
jobId: BigInt(match[3]),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const compareLogicalRun = (left, right) => {
|
|
40
|
+
if (left.pipelineId > right.pipelineId) {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
if (left.pipelineId < right.pipelineId) {
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
if (left.jobId > right.jobId) {
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
if (left.jobId < right.jobId) {
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
};
|
|
54
|
+
const currentMarker = (client) => ({
|
|
55
|
+
jobName: Buffer.from(client.ci.ciJobName).toString("base64"),
|
|
56
|
+
pipelineId: BigInt(client.ci.jobRunUid),
|
|
57
|
+
jobId: BigInt(client.ci.currentJobId),
|
|
58
|
+
});
|
|
59
|
+
const notePath = (client, iid, page) => {
|
|
60
|
+
const base = `/projects/${encodePathSegment(client.ci.projectId)}/merge_requests/${encodePathSegment(iid)}/notes`;
|
|
61
|
+
return page === undefined ? base : `${base}?per_page=${NOTE_PAGE_SIZE}&page=${page}`;
|
|
62
|
+
};
|
|
63
|
+
const readOwnedNotes = async (client, iid, currentJobName) => {
|
|
64
|
+
const notes = [];
|
|
65
|
+
let page = 1;
|
|
66
|
+
for (let index = 0; index < MAX_NOTE_SCAN_PAGES; index += 1) {
|
|
67
|
+
const { data, headers } = await client.requestJson("GET", notePath(client, iid, page));
|
|
68
|
+
for (const note of parseNotes(data)) {
|
|
69
|
+
const marker = parseMarker(note.body);
|
|
70
|
+
if (marker?.jobName === currentJobName) {
|
|
71
|
+
notes.push({ note, marker });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const nextPageHeader = headers["x-next-page"];
|
|
75
|
+
if (typeof nextPageHeader !== "string") {
|
|
76
|
+
throw new Error("incomplete note scan");
|
|
77
|
+
}
|
|
78
|
+
const nextPage = nextPageHeader.trim();
|
|
79
|
+
if (!nextPage) {
|
|
80
|
+
return notes;
|
|
81
|
+
}
|
|
82
|
+
const expectedNextPage = String(page + 1);
|
|
83
|
+
if (!isDecimalString(nextPage) || nextPage !== expectedNextPage) {
|
|
84
|
+
throw new Error("incomplete note scan");
|
|
85
|
+
}
|
|
86
|
+
page += 1;
|
|
87
|
+
}
|
|
88
|
+
throw new Error("incomplete note scan");
|
|
89
|
+
};
|
|
90
|
+
const selectNewestOwnedNote = (notes) => notes.reduce((selected, candidate) => {
|
|
91
|
+
if (!selected || compareLogicalRun(candidate.marker, selected.marker) > 0) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
return selected;
|
|
95
|
+
}, undefined);
|
|
96
|
+
export const upsertGitlabJobNote = async (options) => {
|
|
97
|
+
const iid = gitlab.pullRequest?.id;
|
|
98
|
+
if (!iid || !isDecimalString(iid)) {
|
|
99
|
+
throw new Error("missing merge request");
|
|
100
|
+
}
|
|
101
|
+
if (gitlab.projectId && gitlab.mergeRequestProjectId && gitlab.mergeRequestProjectId !== gitlab.projectId) {
|
|
102
|
+
throw new Error("cross-project merge request");
|
|
103
|
+
}
|
|
104
|
+
const client = createGitlabClient(options);
|
|
105
|
+
if (!client.ci.currentJobId || !isDecimalString(client.ci.currentJobId)) {
|
|
106
|
+
throw new Error("missing current job");
|
|
107
|
+
}
|
|
108
|
+
const current = currentMarker(client);
|
|
109
|
+
const reportUrl = new URL(options.reportUrl);
|
|
110
|
+
if ((reportUrl.protocol !== "http:" && reportUrl.protocol !== "https:") || reportUrl.username || reportUrl.password) {
|
|
111
|
+
throw new Error("invalid report URL");
|
|
112
|
+
}
|
|
113
|
+
const body = `${ownershipMarker(current)}\n${renderGitlabReportSummary(options.summary, reportUrl)}`;
|
|
114
|
+
if (body.length > MAX_COMMENT_LENGTH) {
|
|
115
|
+
throw new Error("comment too large");
|
|
116
|
+
}
|
|
117
|
+
const ownedNotes = await readOwnedNotes(client, iid, current.jobName);
|
|
118
|
+
const selected = selectNewestOwnedNote(ownedNotes);
|
|
119
|
+
if (selected && compareLogicalRun(selected.marker, current) > 0) {
|
|
120
|
+
throw new Error("newer owned note exists");
|
|
121
|
+
}
|
|
122
|
+
if (selected) {
|
|
123
|
+
await client.requestJson("PUT", `${notePath(client, iid)}/${encodePathSegment(selected.note.id)}`, { body });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await client.requestJson("POST", notePath(client, iid), { body });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, relative, sep } from "node:path";
|
|
3
|
+
import { createGitlabClient } from "./client.js";
|
|
4
|
+
const previousGitlabJobQuery = `query PreviousGitlabJob($path: ID!, $ref: String!, $source: String!, $job: String!) {
|
|
5
|
+
project(fullPath: $path) {
|
|
6
|
+
pipelines(ref: $ref, source: $source, first: 100) {
|
|
7
|
+
nodes { id source job(name: $job) { id name status retried } }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}`;
|
|
11
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
12
|
+
const decodeGitlabId = (value, type) => {
|
|
13
|
+
if (typeof value !== "string") {
|
|
14
|
+
throw new Error("invalid id");
|
|
15
|
+
}
|
|
16
|
+
const typeName = type === "Pipeline" ? "Pipeline" : "Build";
|
|
17
|
+
const match = value.match(new RegExp(`^gid://gitlab/Ci::${typeName}/([0-9]+)$`));
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error("invalid id");
|
|
20
|
+
}
|
|
21
|
+
return { text: match[1], value: BigInt(match[1]) };
|
|
22
|
+
};
|
|
23
|
+
const parseJob = (value) => {
|
|
24
|
+
if (value === null) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (!isRecord(value)) {
|
|
28
|
+
throw new Error("invalid job");
|
|
29
|
+
}
|
|
30
|
+
const { id, name, status, retried } = value;
|
|
31
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof status !== "string") {
|
|
32
|
+
throw new Error("invalid job");
|
|
33
|
+
}
|
|
34
|
+
if (retried !== true && retried !== false && retried !== null) {
|
|
35
|
+
throw new Error("invalid job");
|
|
36
|
+
}
|
|
37
|
+
return { id, name, status, retried };
|
|
38
|
+
};
|
|
39
|
+
const parsePipelines = (response) => {
|
|
40
|
+
if (!isRecord(response) || !isRecord(response.project)) {
|
|
41
|
+
throw new Error("invalid project");
|
|
42
|
+
}
|
|
43
|
+
const { pipelines } = response.project;
|
|
44
|
+
if (!isRecord(pipelines) || !Array.isArray(pipelines.nodes)) {
|
|
45
|
+
throw new Error("invalid pipelines");
|
|
46
|
+
}
|
|
47
|
+
return pipelines.nodes.map((node) => {
|
|
48
|
+
if (!isRecord(node) ||
|
|
49
|
+
typeof node.id !== "string" ||
|
|
50
|
+
typeof node.source !== "string" ||
|
|
51
|
+
!Object.hasOwn(node, "job")) {
|
|
52
|
+
throw new Error("invalid pipeline");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
id: node.id,
|
|
56
|
+
source: node.source,
|
|
57
|
+
job: parseJob(node.job),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const selectPreviousJobId = (client, nodes) => {
|
|
62
|
+
const currentPipelineId = BigInt(client.ci.jobRunUid);
|
|
63
|
+
let previousPipelineId;
|
|
64
|
+
let selectedJobId;
|
|
65
|
+
for (const node of nodes) {
|
|
66
|
+
const pipelineId = decodeGitlabId(node.id, "Pipeline");
|
|
67
|
+
if (previousPipelineId !== undefined && pipelineId.value >= previousPipelineId) {
|
|
68
|
+
throw new Error("invalid pipeline order");
|
|
69
|
+
}
|
|
70
|
+
previousPipelineId = pipelineId.value;
|
|
71
|
+
if (node.source !== client.ci.pipelineSource) {
|
|
72
|
+
throw new Error("invalid pipeline source");
|
|
73
|
+
}
|
|
74
|
+
const { job } = node;
|
|
75
|
+
const jobId = job ? decodeGitlabId(job.id, "Build") : undefined;
|
|
76
|
+
if (selectedJobId || pipelineId.value >= currentPipelineId || !job || !jobId) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (job.name !== client.ci.ciJobName) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (job.status !== "SUCCESS" && job.status !== "FAILED") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (job.retried === true) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
selectedJobId = jobId.text;
|
|
89
|
+
}
|
|
90
|
+
return selectedJobId;
|
|
91
|
+
};
|
|
92
|
+
const projectRelativePath = (projectDirectory, historyPath) => relative(projectDirectory, historyPath).split(sep).join("/");
|
|
93
|
+
export const restoreGitlabHistory = async (options) => {
|
|
94
|
+
const client = createGitlabClient(options);
|
|
95
|
+
const response = await client.query(previousGitlabJobQuery, {
|
|
96
|
+
path: client.ci.projectPath,
|
|
97
|
+
ref: client.ci.ref,
|
|
98
|
+
source: client.ci.pipelineSource,
|
|
99
|
+
job: client.ci.ciJobName,
|
|
100
|
+
});
|
|
101
|
+
const selectedJobId = selectPreviousJobId(client, parsePipelines(response));
|
|
102
|
+
if (!selectedJobId) {
|
|
103
|
+
throw new Error("no matching prior job in returned pipeline window");
|
|
104
|
+
}
|
|
105
|
+
const bytes = await client.downloadArtifact(selectedJobId, projectRelativePath(client.ci.projectDirectory, options.historyPath));
|
|
106
|
+
if (bytes.byteLength === 0) {
|
|
107
|
+
throw new Error("empty history artifact");
|
|
108
|
+
}
|
|
109
|
+
await mkdir(dirname(options.historyPath), { recursive: true });
|
|
110
|
+
await writeFile(options.historyPath, bytes);
|
|
111
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { renderReportSummaryMarkdown } from "../../reportMarkdown.js";
|
|
2
|
+
const reportHref = (report, reportUrl) => {
|
|
3
|
+
const { href, reportPath } = report;
|
|
4
|
+
if (href && /^[A-Za-z][A-Za-z0-9+.-]*:/.test(href)) {
|
|
5
|
+
return href;
|
|
6
|
+
}
|
|
7
|
+
if (reportPath !== undefined) {
|
|
8
|
+
const path = reportPath.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
9
|
+
return new URL(`${path ? `${path}/` : ""}index.html`, reportUrl).href;
|
|
10
|
+
}
|
|
11
|
+
return href ? new URL(href, reportUrl).href : undefined;
|
|
12
|
+
};
|
|
13
|
+
export const renderGitlabReportSummary = (summary, reportUrl) => {
|
|
14
|
+
const context = {
|
|
15
|
+
...summary,
|
|
16
|
+
reports: summary.reports.map((report) => ({ ...report, href: reportHref(report, reportUrl) })),
|
|
17
|
+
};
|
|
18
|
+
return renderReportSummaryMarkdown(context, { includeArtifacts: false });
|
|
19
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export * from "./reportContext.js";
|
|
2
|
+
export * from "./reportMarkdown.js";
|
|
1
3
|
export { applyAllureCiEnv } from "./ciEnv.js";
|
|
2
4
|
export { detect } from "./detect.js";
|
|
3
5
|
export { isLocalCiDescriptor } from "./detectors/local.js";
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
+
export { restoreGitlabHistory, upsertGitlabJobNote } from "./helpers/gitlab/index.js";
|
|
7
|
+
export type { GitlabCiDescriptor } from "./detectors/gitlab.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export * from "./reportContext.js";
|
|
2
|
+
export * from "./reportMarkdown.js";
|
|
1
3
|
export { applyAllureCiEnv } from "./ciEnv.js";
|
|
2
4
|
export { detect } from "./detect.js";
|
|
3
5
|
export { isLocalCiDescriptor } from "./detectors/local.js";
|
|
4
|
-
export
|
|
5
|
-
export * from "./reportMarkdown.js";
|
|
6
|
+
export { restoreGitlabHistory, upsertGitlabJobNote } from "./helpers/gitlab/index.js";
|
package/dist/reportContext.js
CHANGED
|
@@ -27,6 +27,7 @@ const emptyResolutionStats = () => ({
|
|
|
27
27
|
});
|
|
28
28
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
29
|
const isTestStatus = (value) => typeof value === "string" && TEST_STATUSES.includes(value);
|
|
30
|
+
const getFiniteNumber = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
30
31
|
const normalizePath = (filePath) => filePath.split(sep).join("/");
|
|
31
32
|
const readOptionalJson = async (filePath, onError) => {
|
|
32
33
|
if (!existsSync(filePath)) {
|
|
@@ -40,12 +41,43 @@ const readOptionalJson = async (filePath, onError) => {
|
|
|
40
41
|
return undefined;
|
|
41
42
|
}
|
|
42
43
|
};
|
|
44
|
+
const getReportName = (summary) => {
|
|
45
|
+
if (typeof summary.name === "string" && summary.name.trim()) {
|
|
46
|
+
return summary.name;
|
|
47
|
+
}
|
|
48
|
+
return "Allure Report";
|
|
49
|
+
};
|
|
50
|
+
const hasPluginSummarySignal = (value) => typeof value.name === "string" ||
|
|
51
|
+
typeof value.plugin === "string" ||
|
|
52
|
+
typeof value.href === "string" ||
|
|
53
|
+
typeof value.remoteHref === "string" ||
|
|
54
|
+
isRecord(value.stats);
|
|
43
55
|
const isPluginSummary = (value) => {
|
|
44
|
-
if (!isRecord(value) ||
|
|
56
|
+
if (!isRecord(value) || !hasPluginSummarySignal(value)) {
|
|
45
57
|
return false;
|
|
46
58
|
}
|
|
47
|
-
return
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
const normalizePluginSummaryStats = (summary) => {
|
|
62
|
+
const rawStats = isRecord(summary.stats) ? summary.stats : {};
|
|
63
|
+
const stats = {
|
|
64
|
+
...rawStats,
|
|
65
|
+
failed: getFiniteNumber(rawStats.failed),
|
|
66
|
+
broken: getFiniteNumber(rawStats.broken),
|
|
67
|
+
passed: getFiniteNumber(rawStats.passed),
|
|
68
|
+
skipped: getFiniteNumber(rawStats.skipped),
|
|
69
|
+
unknown: getFiniteNumber(rawStats.unknown),
|
|
70
|
+
};
|
|
71
|
+
stats.total = getFiniteNumber(rawStats.total, TEST_STATUSES.reduce((acc, status) => acc + getFiniteNumber(stats[status]), 0));
|
|
72
|
+
return stats;
|
|
48
73
|
};
|
|
74
|
+
const normalizePluginSummary = (summary) => ({
|
|
75
|
+
...summary,
|
|
76
|
+
name: getReportName(summary),
|
|
77
|
+
stats: normalizePluginSummaryStats(summary),
|
|
78
|
+
status: isTestStatus(summary.status) ? summary.status : "passed",
|
|
79
|
+
duration: getFiniteNumber(summary.duration),
|
|
80
|
+
});
|
|
49
81
|
const getReportPath = (reportDir, summaryFile) => normalizePath(relative(resolve(reportDir), dirname(resolve(summaryFile))));
|
|
50
82
|
const readPluginSummary = async (reportDir, filePath, onError) => {
|
|
51
83
|
const value = await readOptionalJson(filePath, onError);
|
|
@@ -57,7 +89,7 @@ const readPluginSummary = async (reportDir, filePath, onError) => {
|
|
|
57
89
|
return undefined;
|
|
58
90
|
}
|
|
59
91
|
return {
|
|
60
|
-
...value,
|
|
92
|
+
...normalizePluginSummary(value),
|
|
61
93
|
summaryFile: filePath,
|
|
62
94
|
reportPath: getReportPath(reportDir, filePath),
|
|
63
95
|
};
|
|
@@ -204,8 +236,10 @@ const createEnvironmentContext = (registry, summaries) => {
|
|
|
204
236
|
});
|
|
205
237
|
return [...environmentsByName.values()].toSorted((left, right) => left.name.localeCompare(right.name));
|
|
206
238
|
};
|
|
207
|
-
const createReport = (summary) => (
|
|
208
|
-
const sortReports = (reports) => reports
|
|
239
|
+
const createReport = (summary) => normalizePluginSummary(summary);
|
|
240
|
+
const sortReports = (reports) => reports
|
|
241
|
+
.map((report) => normalizePluginSummary(report))
|
|
242
|
+
.toSorted((left, right) => left.name.localeCompare(right.name));
|
|
209
243
|
const getResolutionStats = (summaries) => {
|
|
210
244
|
const stats = emptyResolutionStats();
|
|
211
245
|
summaries.forEach((summary) => {
|
package/dist/reportMarkdown.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import type { ReportContext } from "./reportContext.js";
|
|
1
|
+
import type { ReportContext, ReportContextFlagStats, ReportContextTestReport, ReportContextResolutionStats, ReportContextStatusStats } from "./reportContext.js";
|
|
2
|
+
export type ReportSummaryMarkdownRow = {
|
|
3
|
+
kind: "total" | "environment" | "report";
|
|
4
|
+
name: string;
|
|
5
|
+
duration: number;
|
|
6
|
+
stats: ReportContextStatusStats;
|
|
7
|
+
flags: ReportContextFlagStats;
|
|
8
|
+
resolutions?: ReportContextResolutionStats;
|
|
9
|
+
report?: ReportContextTestReport;
|
|
10
|
+
};
|
|
2
11
|
export type RenderReportSummaryMarkdownOptions = {
|
|
3
12
|
title?: string;
|
|
4
13
|
includeArtifacts?: boolean;
|
|
14
|
+
getReportFilterHref?: (filter: keyof ReportContextFlagStats, row: ReportSummaryMarkdownRow) => string | undefined;
|
|
5
15
|
};
|
|
6
16
|
export declare const renderReportSummaryMarkdown: (context: ReportContext, options?: RenderReportSummaryMarkdownOptions) => string;
|
package/dist/reportMarkdown.js
CHANGED
|
@@ -10,6 +10,7 @@ const STATUS_ICON_BASE_URL = "https://allurecharts.qameta.workers.dev/dot";
|
|
|
10
10
|
const STATUS_PIE_BASE_URL = "https://allurecharts.qameta.workers.dev/pie";
|
|
11
11
|
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
12
12
|
const tableCell = (value) => escapeHtml(String(value)).replaceAll("|", "|").replaceAll("\n", "<br>");
|
|
13
|
+
const inlineCode = (value) => `<code>${escapeHtml(value)}</code>`;
|
|
13
14
|
const isSafeHref = (href) => {
|
|
14
15
|
try {
|
|
15
16
|
const url = new URL(href);
|
|
@@ -82,11 +83,19 @@ const formatResolutions = (resolutions) => {
|
|
|
82
83
|
return lines.join("<br>");
|
|
83
84
|
};
|
|
84
85
|
const hasResolutions = (resolutions) => resolutions.issues > 0 || resolutions.muted > 0 || resolutions.accepted > 0;
|
|
85
|
-
const formatFlag = (
|
|
86
|
-
const
|
|
86
|
+
const formatFlag = (flag, row, options) => {
|
|
87
|
+
const value = row.flags[flag];
|
|
88
|
+
if (value <= 0) {
|
|
89
|
+
return "0";
|
|
90
|
+
}
|
|
91
|
+
const label = String(value);
|
|
92
|
+
const href = options.getReportFilterHref?.(flag, row);
|
|
93
|
+
return href ? link(label, href) : label;
|
|
94
|
+
};
|
|
95
|
+
const renderTable = (rows, includeResolutions, options) => {
|
|
87
96
|
const headers = [
|
|
88
97
|
" ",
|
|
89
|
-
"
|
|
98
|
+
"Environment",
|
|
90
99
|
"Duration",
|
|
91
100
|
"Stats",
|
|
92
101
|
...(includeResolutions ? ["Resolutions"] : []),
|
|
@@ -101,14 +110,31 @@ const renderTable = (rows, includeResolutions) => {
|
|
|
101
110
|
tableCell(formatDuration(row.duration)),
|
|
102
111
|
formatStats(row.stats),
|
|
103
112
|
...(includeResolutions ? [formatResolutions(row.resolutions)] : []),
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
formatFlag("new", row, options),
|
|
114
|
+
formatFlag("flaky", row, options),
|
|
115
|
+
formatFlag("retry", row, options),
|
|
107
116
|
].join(" | "));
|
|
108
117
|
return [`| ${headers.join(" | ")} |`, `| ${divider.join(" | ")} |`, ...body.map((row) => `| ${row} |`)].join("\n");
|
|
109
118
|
};
|
|
110
119
|
const reportHref = (report) => report.remoteHref ?? report.href;
|
|
111
120
|
const reportLabel = (report) => report.plugin ?? report.name;
|
|
121
|
+
const isAwesomeReport = (report) => report?.pluginId?.toLowerCase() === "awesome" || report?.plugin?.toLowerCase() === "awesome";
|
|
122
|
+
const appendReportFilter = (href, filter) => {
|
|
123
|
+
const hashIndex = href.indexOf("#");
|
|
124
|
+
const base = hashIndex === -1 ? href : href.slice(0, hashIndex);
|
|
125
|
+
const hash = hashIndex === -1 ? "" : href.slice(hashIndex);
|
|
126
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
127
|
+
const query = filter === "new" ? "transition=new" : `${filter}=true`;
|
|
128
|
+
return `${base}${separator}${query}${hash}`;
|
|
129
|
+
};
|
|
130
|
+
const createDefaultReportFilterHref = (reports) => {
|
|
131
|
+
const defaultReport = reports.find(isAwesomeReport);
|
|
132
|
+
return (filter, row) => {
|
|
133
|
+
const report = isAwesomeReport(row.report) ? row.report : defaultReport;
|
|
134
|
+
const href = report ? reportHref(report) : undefined;
|
|
135
|
+
return href ? appendReportFilter(href, filter) : undefined;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
112
138
|
const reportLinkKind = (report) => report.plugin?.toLowerCase() === "testops" ? "testops" : "report";
|
|
113
139
|
const toReportLink = (report) => {
|
|
114
140
|
const href = reportHref(report);
|
|
@@ -151,43 +177,46 @@ const pluginSummaryToResolutions = (report) => ({
|
|
|
151
177
|
muted: report.stats.resolutions?.muted ?? 0,
|
|
152
178
|
accepted: report.stats.resolutions?.accepted ?? 0,
|
|
153
179
|
});
|
|
154
|
-
const renderFilteredReports = (reports) => {
|
|
180
|
+
const renderFilteredReports = (reports, options) => {
|
|
155
181
|
const rows = reports.map((report) => ({
|
|
182
|
+
kind: "report",
|
|
156
183
|
name: report.name,
|
|
157
184
|
duration: report.duration,
|
|
158
185
|
stats: pluginSummaryToStatusStats(report),
|
|
159
186
|
flags: pluginSummaryToFlags(report),
|
|
160
187
|
resolutions: pluginSummaryToResolutions(report),
|
|
188
|
+
report,
|
|
161
189
|
}));
|
|
162
190
|
if (!rows.length) {
|
|
163
191
|
return undefined;
|
|
164
192
|
}
|
|
165
193
|
const includeResolutions = rows.some(({ resolutions }) => resolutions && hasResolutions(resolutions));
|
|
166
194
|
const links = renderReportLinks(reports);
|
|
167
|
-
return ["**Filtered Reports**", renderTable(rows, includeResolutions), ...links].join("\n\n");
|
|
195
|
+
return ["**Filtered Reports**", renderTable(rows, includeResolutions, options), ...links].join("\n\n");
|
|
168
196
|
};
|
|
169
197
|
const renderArtifacts = (artifacts) => {
|
|
170
198
|
if (!artifacts.length) {
|
|
171
199
|
return undefined;
|
|
172
200
|
}
|
|
173
|
-
const rows = artifacts.map(({ name, path }) =>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
...rows,
|
|
181
|
-
"",
|
|
182
|
-
"</details>",
|
|
183
|
-
].join("\n");
|
|
201
|
+
const rows = artifacts.map(({ name, path }) => {
|
|
202
|
+
const normalizedPath = path.replaceAll("\\", "/");
|
|
203
|
+
const isDuplicateName = name === path || normalizedPath.endsWith(`/${name}`);
|
|
204
|
+
const label = isDuplicateName ? inlineCode(path) : `${inlineCode(name)} — ${inlineCode(path)}`;
|
|
205
|
+
return `- ${label}`;
|
|
206
|
+
});
|
|
207
|
+
return [`<details>`, `<summary>Artifacts used (${artifacts.length})</summary>`, "", ...rows, "", "</details>"].join("\n");
|
|
184
208
|
};
|
|
185
209
|
export const renderReportSummaryMarkdown = (context, options = {}) => {
|
|
186
210
|
const { title = "Allure Report Summary", includeArtifacts = true } = options;
|
|
187
211
|
const regularReports = context.reports.filter((report) => report.filtered !== true);
|
|
188
212
|
const filteredReports = context.reports.filter((report) => report.filtered === true);
|
|
213
|
+
const renderOptions = {
|
|
214
|
+
...options,
|
|
215
|
+
getReportFilterHref: options.getReportFilterHref ?? createDefaultReportFilterHref(regularReports),
|
|
216
|
+
};
|
|
189
217
|
const aggregateRows = [
|
|
190
218
|
{
|
|
219
|
+
kind: "total",
|
|
191
220
|
name: "All tests",
|
|
192
221
|
duration: context.totals.duration,
|
|
193
222
|
stats: context.totals.stats,
|
|
@@ -195,6 +224,7 @@ export const renderReportSummaryMarkdown = (context, options = {}) => {
|
|
|
195
224
|
resolutions: context.totals.resolutions,
|
|
196
225
|
},
|
|
197
226
|
...context.environments.map((environment) => ({
|
|
227
|
+
kind: "environment",
|
|
198
228
|
name: environment.name,
|
|
199
229
|
duration: environment.duration,
|
|
200
230
|
stats: environment.stats,
|
|
@@ -204,9 +234,9 @@ export const renderReportSummaryMarkdown = (context, options = {}) => {
|
|
|
204
234
|
const includeResolutions = hasResolutions(context.totals.resolutions);
|
|
205
235
|
const sections = [
|
|
206
236
|
`# ${escapeHtml(title)}`,
|
|
207
|
-
renderTable(aggregateRows, includeResolutions),
|
|
237
|
+
renderTable(aggregateRows, includeResolutions, renderOptions),
|
|
208
238
|
...renderReportLinks(regularReports),
|
|
209
|
-
renderFilteredReports(filteredReports),
|
|
239
|
+
renderFilteredReports(filteredReports, renderOptions),
|
|
210
240
|
includeArtifacts ? renderArtifacts(context.artifacts) : undefined,
|
|
211
241
|
].filter((section) => Boolean(section));
|
|
212
242
|
return `${sections.join("\n\n")}\n`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/ci",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.1",
|
|
4
4
|
"description": "Utilities set to get useful report data from different CI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
"lint:fix": "oxlint --import-plugin --fix src test features stories"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@allurereport/core-api": "3.
|
|
36
|
-
"@allurereport/plugin-api": "3.
|
|
35
|
+
"@allurereport/core-api": "3.19.1",
|
|
36
|
+
"@allurereport/plugin-api": "3.19.1",
|
|
37
|
+
"axios": "^1.20.0",
|
|
37
38
|
"glob": "^13.0.6"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^20",
|
|
41
42
|
"@vitest/runner": "^2.1.8",
|
|
42
|
-
"@vitest/snapshot": "^
|
|
43
|
+
"@vitest/snapshot": "^5.0.1",
|
|
43
44
|
"allure-js-commons": "^3",
|
|
44
45
|
"allure-vitest": "^3",
|
|
45
46
|
"rimraf": "^6",
|