allure 3.19.1 → 3.20.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 CHANGED
@@ -68,6 +68,11 @@ Repeat `--results-dir` for multiple patterns. CLI overrides `config.resultsDir`.
68
68
 
69
69
  After the tests complete, the report is generated automatically. Existing results from previous runs are ignored, as Allure 3 focuses solely on new data to ensure accurate and up-to-date reporting.
70
70
 
71
+ Allure operational messages use scoped prefixes such as `[AllureRun]:`, `[QualityGate]:`, and `[AllureRerun]:`.
72
+ Set `ALLURE_LOG_LEVEL=debug` to include details such as selected tests and temporary test plan paths. Allure ignores
73
+ the generic `LOG_LEVEL` environment variable. Use `--silent` to hide the nested test process output while keeping
74
+ Allure lifecycle messages visible.
75
+
71
76
  ### Running Tests In Agent Mode
72
77
 
73
78
  When you need agent-friendly markdown output for review, debugging, or scope validation, use the `agent` command:
@@ -2,6 +2,7 @@ import { type ReportContext } from "@allurereport/ci";
2
2
  import type { FullConfig } from "@allurereport/core";
3
3
  export type GenerateResult = {
4
4
  summary?: ReportContext;
5
+ exitCode: 0 | 1;
5
6
  };
6
7
  export declare const generate: (params: {
7
8
  cwd: string;
@@ -1,6 +1,6 @@
1
1
  import { exit } from "node:process";
2
2
  import { createReportContext } from "@allurereport/ci";
3
- import { AllureReport } from "@allurereport/core";
3
+ import { AllureReport, filterFailedQualityGateResults, stringifyQualityGateResults } from "@allurereport/core";
4
4
  import { KnownError } from "@allurereport/service";
5
5
  import { red } from "yoctocolors";
6
6
  import { findFilesByGlobs } from "../../utils/fileSystem.js";
@@ -26,11 +26,25 @@ export const generate = async (params) => {
26
26
  for (const dir of resultDirectories) {
27
27
  await allureReport.readDirectory(dir);
28
28
  }
29
+ let exitCode = 0;
30
+ if (allureReport.hasQualityGate) {
31
+ const trs = await allureReport.store.allTestResults({ includeRetries: false });
32
+ const { results } = await allureReport.validate({
33
+ trs,
34
+ environment: params.config.environment,
35
+ });
36
+ const qualityGateMessage = stringifyQualityGateResults(results);
37
+ if (qualityGateMessage) {
38
+ console.error(qualityGateMessage);
39
+ }
40
+ allureReport.realtimeDispatcher.sendQualityGateResults(results);
41
+ exitCode = filterFailedQualityGateResults(results).length > 0 ? 1 : 0;
42
+ }
29
43
  await allureReport.done();
30
44
  if (!params.collectSummary) {
31
- return;
45
+ return { exitCode };
32
46
  }
33
- const result = {};
47
+ const result = { exitCode };
34
48
  try {
35
49
  const summary = await createReportContext(params.config.output, {
36
50
  onError: (message) => console.warn(message),
@@ -8,6 +8,10 @@ export type TestProcessResult = {
8
8
  fastFailed: boolean;
9
9
  };
10
10
  export type RunLogsMode = "pipe" | "inherit" | "ignore";
11
+ type RunAttempt = {
12
+ current: number;
13
+ total: number;
14
+ };
11
15
  export declare const executeNestedAllureCommand: (params: {
12
16
  command: string;
13
17
  commandArgs: string[];
@@ -26,6 +30,7 @@ export declare const runTests: (params: {
26
30
  silent?: boolean;
27
31
  logs?: RunLogsMode;
28
32
  logProcessExit?: boolean;
33
+ attempt?: RunAttempt;
29
34
  resultsPatterns?: readonly string[];
30
35
  }) => Promise<TestProcessResult | null>;
31
36
  export declare const executeAllureRun: (params: {
@@ -46,3 +51,4 @@ export declare const executeAllureRun: (params: {
46
51
  globalExitCode: ExitCode;
47
52
  testProcessResult: TestProcessResult | null;
48
53
  }>;
54
+ export {};
@@ -1,20 +1,22 @@
1
- import * as console from "node:console";
2
1
  import { randomUUID } from "node:crypto";
3
2
  import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
3
  import { tmpdir } from "node:os";
5
4
  import { join, resolve } from "node:path";
6
5
  import process from "node:process";
6
+ import { Logger } from "@allurereport/cli-commons";
7
7
  import { QualityGateState, filterFailedQualityGateResults, stringifyQualityGateResults, } from "@allurereport/core";
8
- import { createTestPlan } from "@allurereport/core-api";
8
+ import { createTestPlan, formatDuration } from "@allurereport/core-api";
9
9
  import { allureResultsDirectoriesWatcher, delayedFileProcessingWatcher, newFilesInDirectoryWatcher, } from "@allurereport/directory-watcher";
10
10
  import { formatProcessLogAttachmentName } from "@allurereport/plugin-agent";
11
11
  import { BufferResultFile, PathResultFile } from "@allurereport/reader-api";
12
12
  import { KnownError } from "@allurereport/service";
13
- import { red } from "yoctocolors";
14
- import { logTests, runProcess, terminationOf } from "../../utils/index.js";
13
+ import { runProcess, terminationOf } from "../../utils/index.js";
15
14
  import { logError } from "../../utils/logs.js";
16
15
  import { stopProcessTree } from "../../utils/process.js";
17
16
  import { allureResultsDirectoriesGlobWatcher } from "./resultsDiscovery.js";
17
+ const runLogger = new Logger("AllureRun");
18
+ const qualityGateLogger = new Logger("QualityGate");
19
+ const rerunLogger = new Logger("AllureRerun");
18
20
  export const executeNestedAllureCommand = async (params) => {
19
21
  const nestedProcess = runProcess({
20
22
  command: params.command,
@@ -34,7 +36,7 @@ const attachResultsDirectoryWatchers = (params) => {
34
36
  return allureResultsDirectoriesWatcher(params.cwd, params.onUpdate, { indexDelay: 600 });
35
37
  };
36
38
  export const runTests = async (params) => {
37
- const { allureReport, cwd, command, commandArgs, logs, environmentVariables, environment, withQualityGate, silent, logProcessExit = true, resultsPatterns = [], } = params;
39
+ const { allureReport, cwd, command, commandArgs, logs, environmentVariables, environment, withQualityGate, silent, logProcessExit = true, attempt = { current: 1, total: 1 }, resultsPatterns = [], } = params;
38
40
  let testProcessStarted = false;
39
41
  const allureResultsWatchers = new Map();
40
42
  const processWatcher = delayedFileProcessingWatcher(async (path) => {
@@ -72,6 +74,16 @@ export const runTests = async (params) => {
72
74
  await allureResultsWatch.initialScan();
73
75
  testProcessStarted = true;
74
76
  const beforeProcess = Date.now();
77
+ const commandLine = [command, ...commandArgs].join(" ");
78
+ const rerunsEnabled = attempt.total > 1;
79
+ if (logProcessExit) {
80
+ if (rerunsEnabled) {
81
+ runLogger.info(`Attempt ${attempt.current}/${attempt.total} started: ${commandLine}`);
82
+ }
83
+ else {
84
+ runLogger.info(`Running: ${commandLine}`);
85
+ }
86
+ }
75
87
  const testProcess = runProcess({
76
88
  command,
77
89
  commandArgs,
@@ -107,6 +119,10 @@ export const runTests = async (params) => {
107
119
  fastFailTriggered = true;
108
120
  qualityGateResults = results;
109
121
  qualityGateUnsub = undefined;
122
+ const failedRules = filterFailedQualityGateResults(results).map(({ rule }) => rule);
123
+ const failedRulesMessage = failedRules.length > 0 ? `: ${failedRules.join(", ")}` : "";
124
+ const stopTarget = rerunsEnabled ? `attempt ${attempt.current}/${attempt.total}` : "test process";
125
+ qualityGateLogger.info(`Fast-fail triggered${failedRulesMessage}; stopping ${stopTarget}`);
110
126
  try {
111
127
  await stopProcessTree(testProcess.pid);
112
128
  }
@@ -136,12 +152,16 @@ export const runTests = async (params) => {
136
152
  }
137
153
  const code = await terminationOf(testProcess);
138
154
  const afterProcess = Date.now();
139
- if (logProcessExit) {
140
- if (code !== null) {
141
- console.log(`process finished with code ${code} (${afterProcess - beforeProcess}ms)`);
155
+ if (logProcessExit && rerunsEnabled) {
156
+ const duration = formatDuration(afterProcess - beforeProcess);
157
+ if (fastFailTriggered) {
158
+ runLogger.info(`Attempt ${attempt.current}/${attempt.total} stopped by Quality Gate after ${duration}`);
159
+ }
160
+ else if (code !== null) {
161
+ runLogger.info(`Attempt ${attempt.current}/${attempt.total} finished with code ${code} after ${duration}`);
142
162
  }
143
163
  else {
144
- console.log(`process terminated (${afterProcess - beforeProcess}ms)`);
164
+ runLogger.warn(`Attempt ${attempt.current}/${attempt.total} terminated after ${duration}`);
145
165
  }
146
166
  }
147
167
  await allureResultsWatch.abort();
@@ -159,6 +179,11 @@ export const runTests = async (params) => {
159
179
  fastFailed: fastFailTriggered,
160
180
  };
161
181
  };
182
+ const relatedQualityGateTestResults = async (allureReport, qualityGateResults) => {
183
+ const testResultIds = new Set(filterFailedQualityGateResults(qualityGateResults).flatMap(({ testResults }) => testResults));
184
+ const testResults = await Promise.all([...testResultIds].map((id) => allureReport.store.testResultById(id)));
185
+ return testResults.filter((testResult) => testResult !== undefined);
186
+ };
162
187
  const publishProcessGlobals = (params) => {
163
188
  const { allureReport, command, commandArgs, ignoreLogs, processFailed, testProcessResult } = params;
164
189
  if (!ignoreLogs && testProcessResult?.stdout) {
@@ -182,6 +207,7 @@ const publishProcessGlobals = (params) => {
182
207
  };
183
208
  export const executeAllureRun = async (params) => {
184
209
  const { allureReport, cwd, command, commandArgs, environmentVariables = {}, environment, withQualityGate, silent, logs, ignoreLogs, maxRerun = 0, logProcessExit = true, resultsPatterns = [], } = params;
210
+ const totalAttempts = maxRerun + 1;
185
211
  await allureReport.start();
186
212
  const globalExitCode = {
187
213
  original: 0,
@@ -201,6 +227,10 @@ export const executeAllureRun = async (params) => {
201
227
  environmentVariables,
202
228
  withQualityGate,
203
229
  logProcessExit,
230
+ attempt: {
231
+ current: 1,
232
+ total: totalAttempts,
233
+ },
204
234
  resultsPatterns,
205
235
  });
206
236
  const allFailuresAfterInitialRun = await allureReport.store.failedTestResults();
@@ -218,37 +248,75 @@ export const executeAllureRun = async (params) => {
218
248
  testProcessResult,
219
249
  });
220
250
  }
221
- for (let rerun = 0; rerun < maxRerun && testProcessResult && !testProcessResult.fastFailed; rerun++) {
222
- const failed = await allureReport.store.blockingFailedTestResults();
223
- if (failed.length === 0) {
224
- console.log("no failed tests is detected.");
225
- break;
251
+ for (let rerun = 0; rerun < maxRerun && testProcessResult; rerun++) {
252
+ const nextAttempt = {
253
+ current: rerun + 2,
254
+ total: totalAttempts,
255
+ };
256
+ const fullRerun = testProcessResult.fastFailed;
257
+ let testResultsToRerun = [];
258
+ if (!fullRerun) {
259
+ const blockingFailures = await allureReport.store.blockingFailedTestResults();
260
+ let relatedTestResults = [];
261
+ if (withQualityGate) {
262
+ const currentTestResults = await allureReport.store.allTestResults({ includeRetries: false });
263
+ const { results } = await allureReport.validate({
264
+ trs: currentTestResults,
265
+ environment,
266
+ });
267
+ relatedTestResults = await relatedQualityGateTestResults(allureReport, results);
268
+ }
269
+ testResultsToRerun = [
270
+ ...new Map([...blockingFailures, ...relatedTestResults].map((testResult) => [testResult.id, testResult])).values(),
271
+ ];
272
+ if (testResultsToRerun.length === 0) {
273
+ rerunLogger.info("No blocking failures or failed Quality Gate-related tests remain; no further reruns are needed");
274
+ break;
275
+ }
276
+ }
277
+ const testPlan = createTestPlan(testResultsToRerun);
278
+ if (fullRerun) {
279
+ rerunLogger.warn(`Attempt ${nextAttempt.current}/${nextAttempt.total}: Quality Gate fast-fail interrupted the previous attempt; restarting full test process`);
280
+ }
281
+ else {
282
+ const testWord = testPlan.tests.length === 1 ? "test" : "tests";
283
+ rerunLogger.info(`Attempt ${nextAttempt.current}/${nextAttempt.total}: rerunning ${testPlan.tests.length} failed, broken, or Quality Gate-related ${testWord}`);
284
+ testResultsToRerun.forEach(({ fullName, status }) => {
285
+ rerunLogger.debug(`${fullName} (${status})`);
286
+ });
287
+ }
288
+ const tmpDir = fullRerun ? undefined : await mkdtemp(join(tmpdir(), "allure-run-"));
289
+ const testPlanPath = tmpDir ? resolve(tmpDir, `${rerun}-testplan.json`) : undefined;
290
+ try {
291
+ if (testPlanPath) {
292
+ await writeFile(testPlanPath, JSON.stringify(testPlan));
293
+ rerunLogger.debug(`Test plan: ${testPlanPath}`);
294
+ }
295
+ allureReport.realtimeDispatcher.sendProcessGlobalsReset();
296
+ testProcessResult = await runTests({
297
+ silent,
298
+ logs,
299
+ allureReport,
300
+ cwd,
301
+ command,
302
+ commandArgs,
303
+ environment,
304
+ environmentVariables: {
305
+ ...environmentVariables,
306
+ ...(testPlanPath ? { ALLURE_TESTPLAN_PATH: testPlanPath } : {}),
307
+ ALLURE_RERUN: `${rerun}`,
308
+ },
309
+ withQualityGate,
310
+ logProcessExit,
311
+ attempt: nextAttempt,
312
+ resultsPatterns,
313
+ });
314
+ }
315
+ finally {
316
+ if (tmpDir) {
317
+ await rm(tmpDir, { recursive: true, force: true });
318
+ }
226
319
  }
227
- const testPlan = createTestPlan(failed);
228
- console.log(`rerun number ${rerun} of ${testPlan.tests.length} tests:`);
229
- logTests(failed);
230
- const tmpDir = await mkdtemp(join(tmpdir(), "allure-run-"));
231
- const testPlanPath = resolve(tmpDir, `${rerun}-testplan.json`);
232
- await writeFile(testPlanPath, JSON.stringify(testPlan));
233
- allureReport.realtimeDispatcher.sendProcessGlobalsReset();
234
- testProcessResult = await runTests({
235
- silent,
236
- logs,
237
- allureReport,
238
- cwd,
239
- command,
240
- commandArgs,
241
- environment,
242
- environmentVariables: {
243
- ...environmentVariables,
244
- ALLURE_TESTPLAN_PATH: testPlanPath,
245
- ALLURE_RERUN: `${rerun}`,
246
- },
247
- withQualityGate,
248
- logProcessExit,
249
- resultsPatterns,
250
- });
251
- await rm(tmpDir, { recursive: true });
252
320
  const allFailuresAfterRerun = await allureReport.store.failedTestResults();
253
321
  const blockingFailuresAfterRerun = await allureReport.store.blockingFailedTestResults();
254
322
  publishProcessGlobals({
@@ -261,7 +329,6 @@ export const executeAllureRun = async (params) => {
261
329
  : (testProcessResult?.code ?? -1)) !== 0,
262
330
  testProcessResult,
263
331
  });
264
- logTests(await allureReport.store.allTestResults());
265
332
  }
266
333
  const allFailuresAfterReruns = await allureReport.store.failedTestResults();
267
334
  const blockingFailuresAfterReruns = await allureReport.store.blockingFailedTestResults();
@@ -280,7 +347,7 @@ export const executeAllureRun = async (params) => {
280
347
  if (qualityGateResults.length) {
281
348
  const qualityGateMessage = stringifyQualityGateResults(qualityGateResults);
282
349
  if (qualityGateMessage) {
283
- console.error(qualityGateMessage);
350
+ qualityGateLogger.error(qualityGateMessage);
284
351
  }
285
352
  allureReport.realtimeDispatcher.sendQualityGateResults(qualityGateResults);
286
353
  }
@@ -292,13 +359,13 @@ export const executeAllureRun = async (params) => {
292
359
  catch (error) {
293
360
  globalExitCode.actual = 1;
294
361
  if (error instanceof KnownError) {
295
- console.error(red(error.message));
362
+ runLogger.error(error.message);
296
363
  allureReport.realtimeDispatcher.sendGlobalError({
297
364
  message: error.message,
298
365
  });
299
366
  }
300
367
  else {
301
- await logError("Failed to run tests using Allure due to unexpected error", error);
368
+ await logError("Failed to run tests using Allure due to unexpected error", error, (message) => runLogger.error(message));
302
369
  allureReport.realtimeDispatcher.sendGlobalError({
303
370
  message: error.message,
304
371
  trace: error.stack,
@@ -14,5 +14,5 @@ export declare class GenerateCommand extends Command {
14
14
  historyBaseUrl: string | undefined;
15
15
  hideLabels: string[] | undefined;
16
16
  knownIssues: string | undefined;
17
- execute(): Promise<void>;
17
+ execute(): Promise<0 | 1 | undefined>;
18
18
  }
@@ -59,12 +59,15 @@ export class GenerateCommand extends Command {
59
59
  ...(this.historyBaseUrl !== undefined ? { historyBaseUrl: this.historyBaseUrl } : {}),
60
60
  resolutions: { knownIssuesPath: this.knownIssues },
61
61
  });
62
- await generate({
62
+ const result = await generate({
63
63
  dump: this.dump,
64
64
  resultsDir: this.resultsDir,
65
65
  cwd,
66
66
  config,
67
67
  });
68
+ if (!result) {
69
+ return;
70
+ }
68
71
  if (config.open) {
69
72
  await serve({
70
73
  port: config.port ? parseInt(config.port, 10) : undefined,
@@ -72,12 +75,14 @@ export class GenerateCommand extends Command {
72
75
  open: true,
73
76
  });
74
77
  }
78
+ return result.exitCode;
75
79
  }
76
80
  }
77
81
  GenerateCommand.paths = [["generate"]];
78
82
  GenerateCommand.usage = Command.Usage({
79
83
  description: "Generates the report in the specified directory.",
80
- details: "This command generates a report from the provided Allure Results directories.",
84
+ details: "This command generates a report from the provided Allure Results directories. " +
85
+ "When a quality gate is configured, the command validates the loaded results and finishes writing the report or dump before returning its status.",
81
86
  examples: [
82
87
  ["generate ./allure-results", "Generate a report from the ./allure-results directory"],
83
88
  [
@@ -10,6 +10,7 @@ export declare class GitlabGenerateCommand extends Command {
10
10
  historyPath: string | undefined;
11
11
  historyLimit: string | undefined;
12
12
  historyBaseUrl: string | undefined;
13
+ summaryComment: boolean;
13
14
  gitlabToken: string | undefined;
14
- execute(): Promise<void>;
15
+ execute(): Promise<0 | 1 | undefined>;
15
16
  }
@@ -66,6 +66,9 @@ export class GitlabGenerateCommand extends Command {
66
66
  this.historyBaseUrl = Option.String("--history-base-url", {
67
67
  description: "The public base URL of the generated report directory",
68
68
  });
69
+ this.summaryComment = Option.Boolean("--summary-comment", true, {
70
+ description: "Post a report summary comment in the merge request (default: true). Disable with --no-summary-comment",
71
+ });
69
72
  this.gitlabToken = Option.String("--gitlab-token", {
70
73
  description: "GitLab api token with api write permissions",
71
74
  });
@@ -111,11 +114,11 @@ export class GitlabGenerateCommand extends Command {
111
114
  }
112
115
  const reportUrl = `${config.historyBaseUrl}/index.html`;
113
116
  console.log(`GitLab report URL: ${reportUrl}`);
114
- if (result.summary && existsSync(join(config.output, "index.html"))) {
117
+ if (this.summaryComment && result.summary && existsSync(join(config.output, "index.html"))) {
115
118
  console.log("Posting report summary comment");
116
119
  if (!token) {
117
120
  console.log(" no API token provided, skipping");
118
- return;
121
+ return result.exitCode;
119
122
  }
120
123
  const { summary } = result;
121
124
  await runGitlabOperation(" failed to add summary comment", () => upsertGitlabJobNote({
@@ -124,12 +127,13 @@ export class GitlabGenerateCommand extends Command {
124
127
  reportUrl,
125
128
  }));
126
129
  }
130
+ return result.exitCode;
127
131
  }
128
132
  }
129
133
  GitlabGenerateCommand.paths = [["gitlab"]];
130
134
  GitlabGenerateCommand.usage = Command.Usage({
131
135
  category: "Integrations",
132
- description: "Generate test report and post report summary in merge request comments",
136
+ description: "Generate test report and optionally post report summary in merge request comments",
133
137
  details: "This command generates a report from the provided Allure Results directories. When api access token is configured, " +
134
138
  "integration will post summary as comment for merge request pipelines and attempt to lookup history file from previously executed job ." +
135
139
  "This integration is designed to be executed from within GitLab CI job.",
@@ -1,7 +1,8 @@
1
- import * as console from "node:console";
2
1
  import { realpath, rm } from "node:fs/promises";
3
2
  import process, { exit } from "node:process";
3
+ import { Logger } from "@allurereport/cli-commons";
4
4
  import { AllureReport, isFileNotFoundError, readConfig } from "@allurereport/core";
5
+ import { formatDuration } from "@allurereport/core-api";
5
6
  import Awesome from "@allurereport/plugin-awesome";
6
7
  import { serve } from "@allurereport/static-server";
7
8
  import { Command, Option, UsageError } from "clipanion";
@@ -10,6 +11,7 @@ import { createChildAllureCliEnvironment, getActiveAllureCliCommand } from "../u
10
11
  import { parseRunCommand, resolveResultsPatterns } from "../utils/resultsPatterns.js";
11
12
  import { executeAllureRun, executeNestedAllureCommand } from "./commons/run.js";
12
13
  const missingRunCommandUsageError = () => new UsageError("expecting command to be specified after --, e.g. allure run -- npm run test");
14
+ const runLogger = new Logger("AllureRun");
13
15
  export class RunCommand extends Command {
14
16
  constructor() {
15
17
  super(...arguments);
@@ -32,7 +34,7 @@ export class RunCommand extends Command {
32
34
  description: "The report name (default: Allure Report)",
33
35
  });
34
36
  this.rerun = Option.String("--rerun", {
35
- description: "The number of reruns for failed tests. Quality gate validation is skipped when rerun is greater than 0 (default: 0)",
37
+ description: "The maximum number of reruns for failed tests and Quality Gate fast-fails (default: 0)",
36
38
  });
37
39
  this.silent = Option.Boolean("--silent", {
38
40
  description: "Don't pipe the process output logs to console (default: 0)",
@@ -74,14 +76,10 @@ export class RunCommand extends Command {
74
76
  throw missingRunCommandUsageError();
75
77
  }
76
78
  const before = new Date().getTime();
77
- process.on("exit", (exitCode) => {
78
- const after = new Date().getTime();
79
- console.log(`exit code ${exitCode} (${after - before}ms)`);
80
- });
81
79
  const cwd = await realpath(this.cwd ?? process.cwd());
82
80
  const hideLabels = this.hideLabels?.length ? this.hideLabels : undefined;
83
- console.log(`${command} ${commandArgs.join(" ")}`);
84
81
  if (getActiveAllureCliCommand()) {
82
+ runLogger.info(`Running nested command: ${[command, ...commandArgs].join(" ")}`);
85
83
  const exitCode = await executeNestedAllureCommand({
86
84
  command,
87
85
  commandArgs,
@@ -109,17 +107,13 @@ export class RunCommand extends Command {
109
107
  });
110
108
  const resultsPatterns = resolveResultsPatterns(this.resultsDir ?? [], config.resultsDir);
111
109
  const resolvedEnvironment = resolveCommandEnvironment(config, environmentOptions);
112
- const withRerun = maxRerun > 0;
113
- const withQualityGate = !!config.qualityGate && !withRerun;
114
- if (config.qualityGate && withRerun) {
115
- console.warn("Quality gate doesn't work with rerun; skipping quality gate validation.");
116
- }
110
+ const withQualityGate = !!config.qualityGate;
117
111
  try {
118
112
  await rm(config.output, { recursive: true });
119
113
  }
120
114
  catch (e) {
121
115
  if (!isFileNotFoundError(e)) {
122
- console.error("could not clean output directory", e);
116
+ runLogger.error(`Could not clean output directory: ${e instanceof Error ? e.message : String(e)}`);
123
117
  }
124
118
  }
125
119
  const allureReport = new AllureReport({
@@ -157,6 +151,8 @@ export class RunCommand extends Command {
157
151
  maxRerun,
158
152
  resultsPatterns,
159
153
  });
154
+ const finalExitCode = globalExitCode.actual ?? globalExitCode.original;
155
+ runLogger.info(`Completed with exit code ${finalExitCode} after ${formatDuration(Date.now() - before)}`);
160
156
  if (config.open) {
161
157
  await serve({
162
158
  port: config.port ? parseInt(config.port, 10) : undefined,
@@ -165,7 +161,7 @@ export class RunCommand extends Command {
165
161
  });
166
162
  }
167
163
  else {
168
- exit(globalExitCode.actual ?? globalExitCode.original);
164
+ exit(finalExitCode);
169
165
  }
170
166
  }
171
167
  }
@@ -1,5 +1,4 @@
1
1
  export * from "./process.js";
2
- export * from "./terminal.js";
3
2
  export * from "./logs.js";
4
3
  export * from "./execution-context.js";
5
4
  export * from "./fileSystem.js";
@@ -1,5 +1,4 @@
1
1
  export * from "./process.js";
2
- export * from "./terminal.js";
3
2
  export * from "./logs.js";
4
3
  export * from "./execution-context.js";
5
4
  export * from "./fileSystem.js";
@@ -1,3 +1,3 @@
1
1
  export declare const getLogFileName: () => string;
2
2
  export declare const readLogs: () => Promise<string>;
3
- export declare const logError: (message: string, error: Error) => Promise<void>;
3
+ export declare const logError: (message: string, error: Error, log?: (formattedMessage: string) => void) => Promise<void>;
@@ -11,11 +11,13 @@ export const readLogs = async () => {
11
11
  const logs = await readFile(logFilePath, "utf-8");
12
12
  return logs;
13
13
  }
14
- catch (err) {
14
+ catch {
15
15
  return "";
16
16
  }
17
17
  };
18
- export const logError = async (message, error) => {
18
+ export const logError = async (message, error, log = (formattedMessage) => {
19
+ console.error(red(formattedMessage));
20
+ }) => {
19
21
  let logs = await readLogs();
20
22
  if (!logs) {
21
23
  logs += `${new Date().toISOString()}[ERROR] ${error.message}\n`;
@@ -28,5 +30,5 @@ export const logError = async (message, error) => {
28
30
  }
29
31
  const logFilePath = join(LOGS_DIRECTORY, getLogFileName());
30
32
  await writeFile(logFilePath, logs, "utf-8");
31
- console.error(red(`${message}. Check logs for more details: ${logFilePath}`));
33
+ log(`${message}. Check logs for more details: ${logFilePath}`);
32
34
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allure",
3
- "version": "3.19.1",
3
+ "version": "3.20.1",
4
4
  "description": "Allure Commandline Tool",
5
5
  "keywords": [
6
6
  "allure",
@@ -32,26 +32,27 @@
32
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
33
33
  },
34
34
  "dependencies": {
35
- "@allurereport/charts-api": "3.19.1",
36
- "@allurereport/ci": "3.19.1",
37
- "@allurereport/core": "3.19.1",
38
- "@allurereport/core-api": "3.19.1",
39
- "@allurereport/directory-watcher": "3.19.1",
40
- "@allurereport/plugin-agent": "3.19.1",
41
- "@allurereport/plugin-allure2": "3.19.1",
42
- "@allurereport/plugin-api": "3.19.1",
43
- "@allurereport/plugin-awesome": "3.19.1",
44
- "@allurereport/plugin-classic": "3.19.1",
45
- "@allurereport/plugin-csv": "3.19.1",
46
- "@allurereport/plugin-dashboard": "3.19.1",
47
- "@allurereport/plugin-jira": "3.19.1",
48
- "@allurereport/plugin-log": "3.19.1",
49
- "@allurereport/plugin-progress": "3.19.1",
50
- "@allurereport/plugin-server-reload": "3.19.1",
51
- "@allurereport/plugin-slack": "3.19.1",
52
- "@allurereport/reader-api": "3.19.1",
53
- "@allurereport/service": "3.19.1",
54
- "@allurereport/static-server": "3.19.1",
35
+ "@allurereport/charts-api": "3.20.1",
36
+ "@allurereport/ci": "3.20.1",
37
+ "@allurereport/cli-commons": "3.20.1",
38
+ "@allurereport/core": "3.20.1",
39
+ "@allurereport/core-api": "3.20.1",
40
+ "@allurereport/directory-watcher": "3.20.1",
41
+ "@allurereport/plugin-agent": "3.20.1",
42
+ "@allurereport/plugin-allure2": "3.20.1",
43
+ "@allurereport/plugin-api": "3.20.1",
44
+ "@allurereport/plugin-awesome": "3.20.1",
45
+ "@allurereport/plugin-classic": "3.20.1",
46
+ "@allurereport/plugin-csv": "3.20.1",
47
+ "@allurereport/plugin-dashboard": "3.20.1",
48
+ "@allurereport/plugin-jira": "3.20.1",
49
+ "@allurereport/plugin-log": "3.20.1",
50
+ "@allurereport/plugin-progress": "3.20.1",
51
+ "@allurereport/plugin-server-reload": "3.20.1",
52
+ "@allurereport/plugin-slack": "3.20.1",
53
+ "@allurereport/reader-api": "3.20.1",
54
+ "@allurereport/service": "3.20.1",
55
+ "@allurereport/static-server": "3.20.1",
55
56
  "adm-zip": "^0.6.0",
56
57
  "clipanion": "^4.0.0-rc.4",
57
58
  "glob": "^13.0.6",
@@ -1,3 +0,0 @@
1
- import type { TestResult, TestStatus } from "@allurereport/core-api";
2
- export declare const status2color: (status: TestStatus) => import("yoctocolors").Format;
3
- export declare const logTests: (testResults: TestResult[]) => void;
@@ -1,20 +0,0 @@
1
- import console from "node:console";
2
- import { formatDuration } from "@allurereport/core-api";
3
- import { blue, gray, green, red, yellow } from "yoctocolors";
4
- export const status2color = (status) => {
5
- switch (status) {
6
- case "failed":
7
- return red;
8
- case "broken":
9
- return yellow;
10
- case "passed":
11
- return green;
12
- case "skipped":
13
- return gray;
14
- default:
15
- return blue;
16
- }
17
- };
18
- export const logTests = (testResults) => {
19
- testResults.forEach((tr) => console.log(`${tr.fullName} ${status2color(tr.status)(tr.status)} ${tr.duration !== undefined ? gray(`(${formatDuration(tr.duration)})`) : ""}`));
20
- };