@allurereport/ci 3.17.0 → 3.18.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/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { applyAllureCiEnv } from "./ciEnv.js";
2
2
  export { detect } from "./detect.js";
3
3
  export { isLocalCiDescriptor } from "./detectors/local.js";
4
+ export * from "./reportContext.js";
5
+ export * from "./reportMarkdown.js";
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { applyAllureCiEnv } from "./ciEnv.js";
2
2
  export { detect } from "./detect.js";
3
3
  export { isLocalCiDescriptor } from "./detectors/local.js";
4
+ export * from "./reportContext.js";
5
+ export * from "./reportMarkdown.js";
@@ -0,0 +1,59 @@
1
+ import type { PluginSummary, QualityGateValidationResult, TestResultRegistry } from "@allurereport/plugin-api";
2
+ declare const TEST_STATUSES: ["failed", "broken", "passed", "skipped", "unknown"];
3
+ export type ReportContextStatusStats = Record<(typeof TEST_STATUSES)[number], number> & {
4
+ total: number;
5
+ };
6
+ export type ReportContextFlagStats = {
7
+ new: number;
8
+ flaky: number;
9
+ retry: number;
10
+ };
11
+ export type ReportContextResolutionStats = {
12
+ issues: number;
13
+ muted: number;
14
+ accepted: number;
15
+ };
16
+ export type ReportContextEnvironment = {
17
+ name: string;
18
+ stats: ReportContextStatusStats;
19
+ flags: ReportContextFlagStats;
20
+ duration: number;
21
+ };
22
+ export type ReportContextTestReport = PluginSummary & {
23
+ summaryFile?: string;
24
+ reportPath?: string;
25
+ };
26
+ export type ReportContextArtifact = {
27
+ name: string;
28
+ path: string;
29
+ };
30
+ export type ReportContextTotals = {
31
+ stats: ReportContextStatusStats;
32
+ flags: ReportContextFlagStats;
33
+ resolutions: ReportContextResolutionStats;
34
+ duration: number;
35
+ };
36
+ export type ReportContextQualityGate = QualityGateValidationResult[] | Record<string, QualityGateValidationResult[]>;
37
+ export type ReportContext = {
38
+ reports: ReportContextTestReport[];
39
+ testResults?: TestResultRegistry;
40
+ totals: ReportContextTotals;
41
+ environments: ReportContextEnvironment[];
42
+ artifacts: ReportContextArtifact[];
43
+ qualityGate?: ReportContextQualityGate;
44
+ };
45
+ export type ReportContextData = {
46
+ reports?: ReportContextTestReport[];
47
+ summaries?: PluginSummary[];
48
+ testResults?: TestResultRegistry;
49
+ artifacts?: ReportContextArtifact[];
50
+ qualityGate?: ReportContextQualityGate;
51
+ };
52
+ export type ReadReportContextFilesOptions = {
53
+ onError?: (message: string) => void;
54
+ };
55
+ export type CreateReportContextOptions = ReadReportContextFilesOptions;
56
+ export declare const readReportContextFiles: (reportDir: string, options?: ReadReportContextFilesOptions) => Promise<ReportContextData>;
57
+ export declare const createReportContextFromData: (data: ReportContextData) => ReportContext;
58
+ export declare const createReportContext: (reportDir: string, options?: CreateReportContextOptions) => Promise<ReportContext>;
59
+ export {};
@@ -0,0 +1,252 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { glob } from "glob";
5
+ const TEST_RESULTS_REGISTRY_FILENAME = "test-results.json";
6
+ const QUALITY_GATE_RESULTS_FILENAME = "quality-gate.json";
7
+ const ARTIFACTS_MANIFEST_FILENAME = "artifacts.json";
8
+ const SUMMARY_FILENAME = "summary.json";
9
+ const TEST_STATUSES = ["failed", "broken", "passed", "skipped", "unknown"];
10
+ const emptyStatusStats = () => ({
11
+ failed: 0,
12
+ broken: 0,
13
+ passed: 0,
14
+ skipped: 0,
15
+ unknown: 0,
16
+ total: 0,
17
+ });
18
+ const emptyFlagStats = () => ({
19
+ new: 0,
20
+ flaky: 0,
21
+ retry: 0,
22
+ });
23
+ const emptyResolutionStats = () => ({
24
+ issues: 0,
25
+ muted: 0,
26
+ accepted: 0,
27
+ });
28
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ const isTestStatus = (value) => typeof value === "string" && TEST_STATUSES.includes(value);
30
+ const normalizePath = (filePath) => filePath.split(sep).join("/");
31
+ const readOptionalJson = async (filePath, onError) => {
32
+ if (!existsSync(filePath)) {
33
+ return undefined;
34
+ }
35
+ try {
36
+ return JSON.parse(await readFile(filePath, "utf8"));
37
+ }
38
+ catch (err) {
39
+ onError?.(`Failed to read Allure report context file ${filePath}: ${String(err)}`);
40
+ return undefined;
41
+ }
42
+ };
43
+ const isPluginSummary = (value) => {
44
+ if (!isRecord(value) || typeof value.name !== "string" || !isRecord(value.stats)) {
45
+ return false;
46
+ }
47
+ return typeof value.duration === "number" && isTestStatus(value.status);
48
+ };
49
+ const getReportPath = (reportDir, summaryFile) => normalizePath(relative(resolve(reportDir), dirname(resolve(summaryFile))));
50
+ const readPluginSummary = async (reportDir, filePath, onError) => {
51
+ const value = await readOptionalJson(filePath, onError);
52
+ if (value === undefined) {
53
+ return undefined;
54
+ }
55
+ if (!isPluginSummary(value)) {
56
+ onError?.(`Ignoring unsupported Allure plugin summary file ${filePath}`);
57
+ return undefined;
58
+ }
59
+ return {
60
+ ...value,
61
+ summaryFile: filePath,
62
+ reportPath: getReportPath(reportDir, filePath),
63
+ };
64
+ };
65
+ const findSummaryFiles = async (reportDir) => (await glob(`**/${SUMMARY_FILENAME}`, {
66
+ absolute: true,
67
+ cwd: reportDir,
68
+ ignore: [`**/widgets/${SUMMARY_FILENAME}`],
69
+ nodir: true,
70
+ })).toSorted((left, right) => left.localeCompare(right));
71
+ const normalizeTestResultRegistry = (value, onError) => {
72
+ if (!isRecord(value) || !isRecord(value.byId)) {
73
+ if (value !== undefined) {
74
+ onError?.("Ignoring unsupported Allure test result registry shape");
75
+ }
76
+ return undefined;
77
+ }
78
+ return {
79
+ byId: value.byId,
80
+ };
81
+ };
82
+ const normalizeArtifacts = (value, onError) => {
83
+ if (value === undefined) {
84
+ return [];
85
+ }
86
+ if (!Array.isArray(value)) {
87
+ onError?.("Ignoring unsupported Allure artifacts manifest shape");
88
+ return [];
89
+ }
90
+ const byPath = new Map();
91
+ value.forEach((artifact) => {
92
+ if (!isRecord(artifact) || typeof artifact.name !== "string" || typeof artifact.path !== "string") {
93
+ return;
94
+ }
95
+ if (!byPath.has(artifact.path)) {
96
+ byPath.set(artifact.path, {
97
+ name: artifact.name,
98
+ path: artifact.path,
99
+ });
100
+ }
101
+ });
102
+ return [...byPath.values()].toSorted((left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name));
103
+ };
104
+ const isQualityGateResult = (value) => {
105
+ if (!isRecord(value)) {
106
+ return false;
107
+ }
108
+ return typeof value.success === "boolean" && typeof value.rule === "string" && typeof value.message === "string";
109
+ };
110
+ const normalizeQualityGate = (value, onError) => {
111
+ if (Array.isArray(value)) {
112
+ return value.filter(isQualityGateResult);
113
+ }
114
+ if (!isRecord(value)) {
115
+ if (value !== undefined) {
116
+ onError?.("Ignoring unsupported Allure quality gate results shape");
117
+ }
118
+ return undefined;
119
+ }
120
+ const entries = Object.entries(value).flatMap(([environment, results]) => Array.isArray(results) ? [[environment, results.filter(isQualityGateResult)]] : []);
121
+ if (!entries.length) {
122
+ if (Object.keys(value).length > 0) {
123
+ onError?.("Ignoring unsupported Allure quality gate results shape");
124
+ }
125
+ return undefined;
126
+ }
127
+ return Object.fromEntries(entries);
128
+ };
129
+ const addStatus = (stats, status) => {
130
+ stats[status] += 1;
131
+ stats.total += 1;
132
+ };
133
+ const getSummaryIdSet = (summaries, key) => new Set(summaries.flatMap((summary) => summary[key] ?? []));
134
+ const applyMaxSummaryStats = (stats, summaries) => {
135
+ summaries.forEach((summary) => {
136
+ TEST_STATUSES.forEach((status) => {
137
+ stats[status] = Math.max(stats[status], summary.stats[status] ?? 0);
138
+ });
139
+ stats.total = Math.max(stats.total, summary.stats.total);
140
+ });
141
+ };
142
+ const createTotals = (registry, summaries) => {
143
+ const totals = {
144
+ stats: emptyStatusStats(),
145
+ flags: emptyFlagStats(),
146
+ resolutions: emptyResolutionStats(),
147
+ duration: 0,
148
+ };
149
+ if (!registry) {
150
+ applyMaxSummaryStats(totals.stats, summaries);
151
+ totals.duration = summaries.reduce((max, summary) => Math.max(max, summary.duration), 0);
152
+ return totals;
153
+ }
154
+ Object.values(registry.byId).forEach((testResult) => {
155
+ if (!isRecord(testResult) || !isTestStatus(testResult.status)) {
156
+ return;
157
+ }
158
+ addStatus(totals.stats, testResult.status);
159
+ if (typeof testResult.duration === "number") {
160
+ totals.duration += testResult.duration;
161
+ }
162
+ });
163
+ return totals;
164
+ };
165
+ const addFlags = (flags, testResultId, newTestIds, flakyTestIds, retryTestIds) => {
166
+ if (newTestIds.has(testResultId)) {
167
+ flags.new += 1;
168
+ }
169
+ if (flakyTestIds.has(testResultId)) {
170
+ flags.flaky += 1;
171
+ }
172
+ if (retryTestIds.has(testResultId)) {
173
+ flags.retry += 1;
174
+ }
175
+ };
176
+ const createEnvironmentContext = (registry, summaries) => {
177
+ if (!registry) {
178
+ return [];
179
+ }
180
+ const newTestIds = getSummaryIdSet(summaries, "newTests");
181
+ const flakyTestIds = getSummaryIdSet(summaries, "flakyTests");
182
+ const retryTestIds = getSummaryIdSet(summaries, "retryTests");
183
+ const environmentsByName = new Map();
184
+ Object.entries(registry.byId).forEach(([testResultId, testResult]) => {
185
+ if (!isRecord(testResult) || !isTestStatus(testResult.status) || typeof testResult.environment !== "string") {
186
+ return;
187
+ }
188
+ const environment = testResult.environment.trim();
189
+ if (!environment || environment === "default") {
190
+ return;
191
+ }
192
+ const context = environmentsByName.get(environment) ?? {
193
+ name: environment,
194
+ stats: emptyStatusStats(),
195
+ flags: emptyFlagStats(),
196
+ duration: 0,
197
+ };
198
+ addStatus(context.stats, testResult.status);
199
+ addFlags(context.flags, testResultId, newTestIds, flakyTestIds, retryTestIds);
200
+ if (typeof testResult.duration === "number") {
201
+ context.duration += testResult.duration;
202
+ }
203
+ environmentsByName.set(environment, context);
204
+ });
205
+ return [...environmentsByName.values()].toSorted((left, right) => left.name.localeCompare(right.name));
206
+ };
207
+ const createReport = (summary) => ({ ...summary });
208
+ const sortReports = (reports) => reports.toSorted((left, right) => left.name.localeCompare(right.name));
209
+ const getResolutionStats = (summaries) => {
210
+ const stats = emptyResolutionStats();
211
+ summaries.forEach((summary) => {
212
+ stats.issues = Math.max(stats.issues, summary.stats.resolutions?.issues ?? 0);
213
+ stats.muted = Math.max(stats.muted, summary.stats.resolutions?.muted ?? 0);
214
+ stats.accepted = Math.max(stats.accepted, summary.stats.resolutions?.accepted ?? 0);
215
+ });
216
+ return stats;
217
+ };
218
+ const applySummaryFlags = (totals, summaries) => {
219
+ totals.flags.new = getSummaryIdSet(summaries, "newTests").size;
220
+ totals.flags.flaky = getSummaryIdSet(summaries, "flakyTests").size;
221
+ totals.flags.retry = getSummaryIdSet(summaries, "retryTests").size;
222
+ totals.resolutions = getResolutionStats(summaries);
223
+ };
224
+ export const readReportContextFiles = async (reportDir, options = {}) => {
225
+ const { onError } = options;
226
+ const summaryFiles = await findSummaryFiles(reportDir);
227
+ const reports = (await Promise.all(summaryFiles.map((summaryFile) => readPluginSummary(reportDir, summaryFile, onError)))).filter((summary) => summary !== undefined);
228
+ const registry = normalizeTestResultRegistry(await readOptionalJson(join(reportDir, TEST_RESULTS_REGISTRY_FILENAME), onError), onError);
229
+ const artifacts = normalizeArtifacts(await readOptionalJson(join(reportDir, ARTIFACTS_MANIFEST_FILENAME), onError), onError);
230
+ const qualityGate = normalizeQualityGate(await readOptionalJson(join(reportDir, QUALITY_GATE_RESULTS_FILENAME), onError), onError);
231
+ return {
232
+ reports,
233
+ testResults: registry,
234
+ artifacts,
235
+ qualityGate,
236
+ };
237
+ };
238
+ export const createReportContextFromData = (data) => {
239
+ const reports = sortReports(data.reports ?? data.summaries?.map(createReport) ?? []);
240
+ const artifacts = [...(data.artifacts ?? [])].toSorted((left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name));
241
+ const totals = createTotals(data.testResults, reports);
242
+ applySummaryFlags(totals, reports);
243
+ return {
244
+ reports,
245
+ testResults: data.testResults,
246
+ totals,
247
+ environments: createEnvironmentContext(data.testResults, reports),
248
+ artifacts,
249
+ qualityGate: data.qualityGate,
250
+ };
251
+ };
252
+ export const createReportContext = async (reportDir, options = {}) => createReportContextFromData(await readReportContextFiles(reportDir, options));
@@ -0,0 +1,6 @@
1
+ import type { ReportContext } from "./reportContext.js";
2
+ export type RenderReportSummaryMarkdownOptions = {
3
+ title?: string;
4
+ includeArtifacts?: boolean;
5
+ };
6
+ export declare const renderReportSummaryMarkdown: (context: ReportContext, options?: RenderReportSummaryMarkdownOptions) => string;
@@ -0,0 +1,213 @@
1
+ const STATUS_ORDER = ["passed", "failed", "broken", "skipped", "unknown"];
2
+ const STATUS_LABELS = {
3
+ passed: "Passed tests",
4
+ failed: "Failed tests",
5
+ broken: "Broken tests",
6
+ skipped: "Skipped tests",
7
+ unknown: "Unknown tests",
8
+ };
9
+ const STATUS_ICON_BASE_URL = "https://allurecharts.qameta.workers.dev/dot";
10
+ const STATUS_PIE_BASE_URL = "https://allurecharts.qameta.workers.dev/pie";
11
+ const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
12
+ const tableCell = (value) => escapeHtml(String(value)).replaceAll("|", "&#124;").replaceAll("\n", "<br>");
13
+ const isSafeHref = (href) => {
14
+ try {
15
+ const url = new URL(href);
16
+ return url.protocol === "http:" || url.protocol === "https:";
17
+ }
18
+ catch {
19
+ return !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(href);
20
+ }
21
+ };
22
+ const link = (label, href) => isSafeHref(href) ? `<a href="${escapeHtml(href)}">${escapeHtml(label)}</a>` : escapeHtml(label);
23
+ const statusIcon = (status) => `<img alt="${STATUS_LABELS[status]}" src="${STATUS_ICON_BASE_URL}?type=${status}&size=8" width="8" height="8" />`;
24
+ const statusPie = (stats) => {
25
+ const total = STATUS_ORDER.reduce((sum, status) => sum + (stats[status] ?? 0), 0);
26
+ if (total === 0) {
27
+ return "";
28
+ }
29
+ const params = new URLSearchParams({
30
+ passed: String(stats.passed ?? 0),
31
+ failed: String(stats.failed ?? 0),
32
+ broken: String(stats.broken ?? 0),
33
+ skipped: String(stats.skipped ?? 0),
34
+ unknown: String(stats.unknown ?? 0),
35
+ size: "32",
36
+ });
37
+ return `<img src="${STATUS_PIE_BASE_URL}?${params.toString()}" width="28px" height="28px" />&nbsp;&nbsp;&nbsp;&nbsp;`;
38
+ };
39
+ const formatDuration = (duration) => {
40
+ if (!Number.isFinite(duration) || duration <= 0) {
41
+ return "0ms";
42
+ }
43
+ if (duration < 1000) {
44
+ return `${Math.round(duration)}ms`;
45
+ }
46
+ const totalSeconds = Math.round(duration / 1000);
47
+ const seconds = totalSeconds % 60;
48
+ const totalMinutes = Math.floor(totalSeconds / 60);
49
+ const minutes = totalMinutes % 60;
50
+ const hours = Math.floor(totalMinutes / 60);
51
+ const parts = [];
52
+ if (hours) {
53
+ parts.push(`${hours}h`);
54
+ }
55
+ if (minutes) {
56
+ parts.push(`${minutes}m`);
57
+ }
58
+ if (seconds || parts.length === 0) {
59
+ parts.push(`${seconds}s`);
60
+ }
61
+ return parts.join(" ");
62
+ };
63
+ const formatStats = (stats) => {
64
+ const lines = [];
65
+ STATUS_ORDER.forEach((status) => {
66
+ const count = stats[status] ?? 0;
67
+ if (count > 0) {
68
+ lines.push(`${statusIcon(status)}&#8288;&nbsp;${count}`);
69
+ }
70
+ });
71
+ return lines.length ? lines.join("<br>") : "0";
72
+ };
73
+ const formatResolutions = (resolutions) => {
74
+ if (!resolutions) {
75
+ return "";
76
+ }
77
+ const lines = [
78
+ resolutions.issues ? `Issues: ${resolutions.issues}` : "",
79
+ resolutions.muted ? `Muted: ${resolutions.muted}` : "",
80
+ resolutions.accepted ? `Accepted: ${resolutions.accepted}` : "",
81
+ ].filter(Boolean);
82
+ return lines.join("<br>");
83
+ };
84
+ const hasResolutions = (resolutions) => resolutions.issues > 0 || resolutions.muted > 0 || resolutions.accepted > 0;
85
+ const formatFlag = (value) => (value > 0 ? String(value) : "0");
86
+ const renderTable = (rows, includeResolutions) => {
87
+ const headers = [
88
+ "&nbsp;&nbsp;&nbsp;&nbsp;",
89
+ "Scope",
90
+ "Duration",
91
+ "Stats",
92
+ ...(includeResolutions ? ["Resolutions"] : []),
93
+ "New",
94
+ "Flaky",
95
+ "Retry",
96
+ ];
97
+ const divider = headers.map(() => "---");
98
+ const body = rows.map((row) => [
99
+ statusPie(row.stats),
100
+ tableCell(row.name),
101
+ tableCell(formatDuration(row.duration)),
102
+ formatStats(row.stats),
103
+ ...(includeResolutions ? [formatResolutions(row.resolutions)] : []),
104
+ tableCell(formatFlag(row.flags.new)),
105
+ tableCell(formatFlag(row.flags.flaky)),
106
+ tableCell(formatFlag(row.flags.retry)),
107
+ ].join(" | "));
108
+ return [`| ${headers.join(" | ")} |`, `| ${divider.join(" | ")} |`, ...body.map((row) => `| ${row} |`)].join("\n");
109
+ };
110
+ const reportHref = (report) => report.remoteHref ?? report.href;
111
+ const reportLabel = (report) => report.plugin ?? report.name;
112
+ const reportLinkKind = (report) => report.plugin?.toLowerCase() === "testops" ? "testops" : "report";
113
+ const toReportLink = (report) => {
114
+ const href = reportHref(report);
115
+ return href
116
+ ? {
117
+ label: reportLabel(report),
118
+ href,
119
+ kind: reportLinkKind(report),
120
+ }
121
+ : undefined;
122
+ };
123
+ const sortLinks = (links) => links.toSorted((left, right) => left.kind.localeCompare(right.kind) || left.label.localeCompare(right.label));
124
+ const renderLinks = (label, links) => {
125
+ if (!links.length) {
126
+ return undefined;
127
+ }
128
+ return `**${escapeHtml(label)}:** ${links.map((item) => link(item.label, item.href)).join(", ")}`;
129
+ };
130
+ const renderReportLinks = (reports) => {
131
+ const links = sortLinks(reports.map(toReportLink).filter((item) => item !== undefined));
132
+ const reportLinks = links.filter(({ kind }) => kind === "report");
133
+ const testOpsLinks = links.filter(({ kind }) => kind === "testops");
134
+ return [renderLinks("Reports", reportLinks), renderLinks("TestOps", testOpsLinks)].filter((line) => line !== undefined);
135
+ };
136
+ const pluginSummaryToStatusStats = (report) => ({
137
+ failed: report.stats.failed ?? 0,
138
+ broken: report.stats.broken ?? 0,
139
+ passed: report.stats.passed ?? 0,
140
+ skipped: report.stats.skipped ?? 0,
141
+ unknown: report.stats.unknown ?? 0,
142
+ total: report.stats.total ?? 0,
143
+ });
144
+ const pluginSummaryToFlags = (report) => ({
145
+ new: report.newTests?.length ?? report.stats.new ?? 0,
146
+ flaky: report.flakyTests?.length ?? report.stats.flaky ?? 0,
147
+ retry: report.retryTests?.length ?? report.stats.retries ?? 0,
148
+ });
149
+ const pluginSummaryToResolutions = (report) => ({
150
+ issues: report.stats.resolutions?.issues ?? 0,
151
+ muted: report.stats.resolutions?.muted ?? 0,
152
+ accepted: report.stats.resolutions?.accepted ?? 0,
153
+ });
154
+ const renderFilteredReports = (reports) => {
155
+ const rows = reports.map((report) => ({
156
+ name: report.name,
157
+ duration: report.duration,
158
+ stats: pluginSummaryToStatusStats(report),
159
+ flags: pluginSummaryToFlags(report),
160
+ resolutions: pluginSummaryToResolutions(report),
161
+ }));
162
+ if (!rows.length) {
163
+ return undefined;
164
+ }
165
+ const includeResolutions = rows.some(({ resolutions }) => resolutions && hasResolutions(resolutions));
166
+ const links = renderReportLinks(reports);
167
+ return ["**Filtered Reports**", renderTable(rows, includeResolutions), ...links].join("\n\n");
168
+ };
169
+ const renderArtifacts = (artifacts) => {
170
+ if (!artifacts.length) {
171
+ return undefined;
172
+ }
173
+ const rows = artifacts.map(({ name, path }) => `| ${tableCell(name)} | ${tableCell(path)} |`);
174
+ return [
175
+ `<details>`,
176
+ `<summary>Artifacts used (${artifacts.length})</summary>`,
177
+ "",
178
+ "| Name | Path |",
179
+ "| --- | --- |",
180
+ ...rows,
181
+ "",
182
+ "</details>",
183
+ ].join("\n");
184
+ };
185
+ export const renderReportSummaryMarkdown = (context, options = {}) => {
186
+ const { title = "Allure Report Summary", includeArtifacts = true } = options;
187
+ const regularReports = context.reports.filter((report) => report.filtered !== true);
188
+ const filteredReports = context.reports.filter((report) => report.filtered === true);
189
+ const aggregateRows = [
190
+ {
191
+ name: "All tests",
192
+ duration: context.totals.duration,
193
+ stats: context.totals.stats,
194
+ flags: context.totals.flags,
195
+ resolutions: context.totals.resolutions,
196
+ },
197
+ ...context.environments.map((environment) => ({
198
+ name: environment.name,
199
+ duration: environment.duration,
200
+ stats: environment.stats,
201
+ flags: environment.flags,
202
+ })),
203
+ ];
204
+ const includeResolutions = hasResolutions(context.totals.resolutions);
205
+ const sections = [
206
+ `# ${escapeHtml(title)}`,
207
+ renderTable(aggregateRows, includeResolutions),
208
+ ...renderReportLinks(regularReports),
209
+ renderFilteredReports(filteredReports),
210
+ includeArtifacts ? renderArtifacts(context.artifacts) : undefined,
211
+ ].filter((section) => Boolean(section));
212
+ return `${sections.join("\n\n")}\n`;
213
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/ci",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
4
4
  "description": "Utilities set to get useful report data from different CI",
5
5
  "keywords": [
6
6
  "allure",
@@ -20,7 +20,9 @@
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "exports": {
23
- ".": "./dist/index.js"
23
+ ".": "./dist/index.js",
24
+ "./report-context": "./dist/reportContext.js",
25
+ "./report-markdown": "./dist/reportMarkdown.js"
24
26
  },
25
27
  "scripts": {
26
28
  "build": "run clean && tsc --project ./tsconfig.json",
@@ -30,7 +32,9 @@
30
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
31
33
  },
32
34
  "dependencies": {
33
- "@allurereport/core-api": "3.17.0"
35
+ "@allurereport/core-api": "3.18.0",
36
+ "@allurereport/plugin-api": "3.18.0",
37
+ "glob": "^13.0.6"
34
38
  },
35
39
  "devDependencies": {
36
40
  "@types/node": "^20",