@allurereport/ci 3.18.0 → 3.19.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.
@@ -1,2 +1,16 @@
1
1
  import { type CiDescriptor } from "@allurereport/core-api";
2
- export declare const gitlab: CiDescriptor;
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;
@@ -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 isTagPipeline = () => Boolean(getEnv("CI_COMMIT_TAG"));
5
- const getSourceBranch = () => {
6
- if (isTagPipeline()) {
7
- return undefined;
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
- return (getEnv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME") ||
10
- getEnv("CI_COMMIT_BRANCH") ||
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 getSourceBranch() || "";
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().replace(/\/+$/, "");
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 getSourceBranch();
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,6 @@
1
+ import type { ReportContext } from "../../reportContext.js";
2
+ export declare const upsertGitlabJobNote: (options: {
3
+ token: string;
4
+ summary: ReportContext;
5
+ reportUrl: string;
6
+ }) => Promise<void>;
@@ -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,4 @@
1
+ export declare const restoreGitlabHistory: (options: {
2
+ token?: string;
3
+ historyPath: string;
4
+ }) => Promise<void>;
@@ -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,4 @@
1
+ export { createGitlabClient } from "./client.js";
2
+ export { upsertGitlabJobNote } from "./comments.js";
3
+ export { restoreGitlabHistory } from "./history.js";
4
+ export type { GitlabClient } from "./client.js";
@@ -0,0 +1,3 @@
1
+ export { createGitlabClient } from "./client.js";
2
+ export { upsertGitlabJobNote } from "./comments.js";
3
+ export { restoreGitlabHistory } from "./history.js";
@@ -0,0 +1,2 @@
1
+ import type { ReportContext } from "../../reportContext.js";
2
+ export declare const renderGitlabReportSummary: (summary: ReportContext, reportUrl: URL) => string;
@@ -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 * from "./reportContext.js";
5
- export * from "./reportMarkdown.js";
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 * from "./reportContext.js";
5
- export * from "./reportMarkdown.js";
6
+ export { restoreGitlabHistory, upsertGitlabJobNote } from "./helpers/gitlab/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/ci",
3
- "version": "3.18.0",
3
+ "version": "3.19.0",
4
4
  "description": "Utilities set to get useful report data from different CI",
5
5
  "keywords": [
6
6
  "allure",
@@ -32,8 +32,9 @@
32
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
33
33
  },
34
34
  "dependencies": {
35
- "@allurereport/core-api": "3.18.0",
36
- "@allurereport/plugin-api": "3.18.0",
35
+ "@allurereport/core-api": "3.19.0",
36
+ "@allurereport/plugin-api": "3.19.0",
37
+ "axios": "^1.20.0",
37
38
  "glob": "^13.0.6"
38
39
  },
39
40
  "devDependencies": {