Skip to content

Commit ed4611c

Browse files
authored
fix(core): improve GitHub Actions summaries (#1648)
1 parent 3ca13bc commit ed4611c

10 files changed

Lines changed: 277 additions & 29 deletions

File tree

e2e/reporter/fixtures-npm/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
include: ['../fixtures/githubActions.test.ts'],
5+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { expect, it } from '@rstest/core';
2+
3+
it('reports a long diff', () => {
4+
expect([
5+
'received first line',
6+
...Array.from(
7+
{ length: 40 },
8+
(_, index) => `received context line ${index}`,
9+
),
10+
'received last line',
11+
]).toEqual([
12+
'expected first line',
13+
...Array.from(
14+
{ length: 40 },
15+
(_, index) => `expected context line ${index}`,
16+
),
17+
'expected last line',
18+
]);
19+
});

e2e/reporter/githubActions.test.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,18 +297,17 @@ it.skipIf(!process.env.CI)(
297297
'.tmp',
298298
'github-step-summary-npm.md',
299299
);
300-
const packageLockPath = join(__dirname, 'package-lock.json');
300+
const npmFixturePath = join(__dirname, 'fixtures-npm');
301301

302302
fs.rmSync(stepSummaryPath, { force: true });
303-
fs.writeFileSync(packageLockPath, '{}');
304303

305304
try {
306305
const { cli } = await runRstestCli({
307306
command: 'rstest',
308-
args: ['run', 'githubActions', '--reporters', 'github-actions'],
307+
args: ['run', '--reporters', 'github-actions'],
309308
options: {
310309
nodeOptions: {
311-
cwd: __dirname,
310+
cwd: npmFixturePath,
312311
env: {
313312
GITHUB_WORKSPACE: githubWorkspace,
314313
GITHUB_STEP_SUMMARY: stepSummaryPath,
@@ -324,10 +323,49 @@ it.skipIf(!process.env.CI)(
324323
const stepSummary = fs.readFileSync(stepSummaryPath, 'utf-8');
325324

326325
expect(stepSummary).toContain(
327-
"npx rstest 'fixtures/githubActions.test.ts' --testNamePattern 'should add two numbers correctly'",
326+
"npx rstest '../fixtures/githubActions.test.ts' --testNamePattern 'should add two numbers correctly'",
328327
);
329328
} finally {
330-
fs.rmSync(packageLockPath, { force: true });
329+
fs.rmSync(stepSummaryPath, { force: true });
330+
fs.rmSync(join(__dirname, '.tmp'), { recursive: true, force: true });
331+
}
332+
},
333+
);
334+
335+
it.skipIf(!process.env.CI)(
336+
'github-actions summary supports a custom field length',
337+
async () => {
338+
const stepSummaryPath = join(
339+
__dirname,
340+
'.tmp',
341+
'github-step-summary-long-diff.md',
342+
);
343+
fs.rmSync(stepSummaryPath, { force: true });
344+
345+
try {
346+
const { cli } = await runRstestCli({
347+
command: 'rstest',
348+
args: ['run', '-c', './rstest.githubActions.longSummary.config.mts'],
349+
options: {
350+
nodeOptions: {
351+
cwd: __dirname,
352+
env: {
353+
GITHUB_WORKSPACE: githubWorkspace,
354+
GITHUB_STEP_SUMMARY: stepSummaryPath,
355+
},
356+
},
357+
},
358+
});
359+
360+
await cli.exec;
361+
await cli.waitForStreamsEnd();
362+
expect(cli.exec.process?.exitCode).toBe(1);
363+
364+
const stepSummary = fs.readFileSync(stepSummaryPath, 'utf-8');
365+
366+
expect(stepSummary).toContain('- "expected first line",');
367+
expect(stepSummary).toContain('+ "received last line",');
368+
} finally {
331369
fs.rmSync(stepSummaryPath, { force: true });
332370
fs.rmSync(join(__dirname, '.tmp'), { recursive: true, force: true });
333371
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from '@rstest/core';
2+
3+
export default defineConfig({
4+
include: ['**/fixtures/summaryTruncation.test.ts'],
5+
reporters: [
6+
[
7+
'github-actions',
8+
{
9+
summary: {
10+
maxCharsPerField: 10_000,
11+
},
12+
},
13+
],
14+
],
15+
});

packages/core/src/reporter/githubActions.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import stripAnsi from 'strip-ansi';
55
import type {
66
Duration,
77
GetSourcemap,
8+
ReporterWithOptions,
89
SnapshotSummary,
910
TestFileResult,
1011
TestResult,
@@ -38,6 +39,7 @@ export class GithubActionsReporter {
3839
private readonly stepSummaryPath?: string;
3940
private readonly enableAnnotations: boolean;
4041
private readonly enableSummary: boolean;
42+
private readonly summaryMaxCharsPerField: number;
4143
private readonly reportName?: string;
4244

4345
constructor({
@@ -49,18 +51,24 @@ export class GithubActionsReporter {
4951
config?: {
5052
name?: string;
5153
};
52-
options: {
54+
options: ReporterWithOptions<'github-actions'>[1] & {
5355
// `createReporters` never supplies this; only tests do. Optional is the truth.
5456
onWritePath?: (path: string) => string;
55-
annotations?: boolean;
56-
summary?: boolean;
5757
};
5858
}) {
5959
this.onWritePath = options.onWritePath;
6060
this.rootPath = rootPath;
6161
this.stepSummaryPath = process.env.GITHUB_STEP_SUMMARY;
6262
this.enableAnnotations = options.annotations !== false;
6363
this.enableSummary = options.summary !== false;
64+
this.summaryMaxCharsPerField =
65+
typeof options.summary === 'object'
66+
? Math.max(
67+
0,
68+
options.summary.maxCharsPerField ??
69+
STEP_SUMMARY_DEFAULT_MAX_CHARS_PER_FIELD,
70+
)
71+
: STEP_SUMMARY_DEFAULT_MAX_CHARS_PER_FIELD;
6472
this.reportName = config?.name;
6573
}
6674

@@ -162,6 +170,7 @@ export class GithubActionsReporter {
162170
failures,
163171
getSourcemap,
164172
unhandledErrors,
173+
maxCharsPerField: this.summaryMaxCharsPerField,
165174
}),
166175
);
167176
}
@@ -179,7 +188,7 @@ function escapeData(s: string): string {
179188

180189
const STEP_SUMMARY_MAX_FAILURES = 20;
181190
const STEP_SUMMARY_MAX_FLAKY_TESTS = 20;
182-
const STEP_SUMMARY_MAX_MESSAGE_LENGTH = 400;
191+
const STEP_SUMMARY_DEFAULT_MAX_CHARS_PER_FIELD = 400;
183192
const STEP_SUMMARY_MAX_FLAKY_MESSAGE_LENGTH = 160;
184193
const ROOT_PATH_PLACEHOLDER = '<ROOT>';
185194
const DEFAULT_PROJECT_NAME = 'rstest';
@@ -274,6 +283,7 @@ async function renderStepSummary({
274283
failures,
275284
getSourcemap,
276285
unhandledErrors,
286+
maxCharsPerField,
277287
}: {
278288
results: TestFileResult[];
279289
testResults: TestResult[];
@@ -283,6 +293,7 @@ async function renderStepSummary({
283293
failures: FailureItem[];
284294
getSourcemap: GetSourcemap;
285295
unhandledErrors?: Error[];
296+
maxCharsPerField: number;
286297
}): Promise<string> {
287298
const { parseErrorStacktrace } = await import('../utils/error');
288299
const packageManagerAgent = await detectPackageManagerAgent(rootPath);
@@ -365,12 +376,19 @@ async function renderStepSummary({
365376

366377
pushHeading(lines, 3, `❌ FAIL Unhandled Error ${index + 1}`);
367378
lines.push(
368-
`**${error.name || 'Error'}**: ${trimForSummary(error.message)}`,
379+
`**${error.name || 'Error'}**: ${trimForSummary(
380+
error.message,
381+
maxCharsPerField,
382+
)}`,
369383
);
370384
lines.push('');
371385

372386
if (error.stack) {
373-
pushFencedBlock(lines, '', stripAnsi(trimForSummary(error.stack)));
387+
pushFencedBlock(
388+
lines,
389+
'',
390+
stripAnsi(trimForSummary(error.stack, maxCharsPerField)),
391+
);
374392
}
375393
}
376394

@@ -398,15 +416,19 @@ async function renderStepSummary({
398416
? errors
399417
: [{ message: 'Unknown error' }]) {
400418
const errorType = getErrorType(error);
401-
const message = trimForSummary(error.message);
419+
const message = trimForSummary(error.message, maxCharsPerField);
402420
const retryLabel = getRetryErrorLabel(error);
403421
lines.push(
404422
`**${retryLabel ? `${retryLabel} - ` : ''}${errorType}**: ${message}`,
405423
);
406424
lines.push('');
407425

408426
if (error.diff) {
409-
pushFencedBlock(lines, 'diff', stripAnsi(trimForSummary(error.diff)));
427+
pushFencedBlock(
428+
lines,
429+
'diff',
430+
stripAnsi(trimForSummary(error.diff, maxCharsPerField)),
431+
);
410432
}
411433

412434
if (error.stack) {
@@ -449,12 +471,16 @@ async function renderStepSummary({
449471
return `${lines.join('\n')}\n`;
450472
}
451473

452-
function trimForSummary(input: string): string {
453-
if (input.length <= STEP_SUMMARY_MAX_MESSAGE_LENGTH) {
474+
function trimForSummary(input: string, maxChars: number): string {
475+
if (input.length <= maxChars) {
454476
return input;
455477
}
456478

457-
return `${input.slice(0, STEP_SUMMARY_MAX_MESSAGE_LENGTH - 1)}…`;
479+
if (maxChars === 0) {
480+
return '';
481+
}
482+
483+
return `${input.slice(0, maxChars - 1)}…`;
458484
}
459485

460486
function collectFlakyTests(testResults: TestResult[]): TestResult[] {

packages/core/src/types/reporter.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,22 @@ type GithubActionsReporterOptions = {
150150
*/
151151
annotations?: boolean;
152152
/**
153-
* Whether to append a Markdown summary to `GITHUB_STEP_SUMMARY`.
153+
* Markdown summary controls.
154+
* - `false`: do not append a summary to `GITHUB_STEP_SUMMARY`
155+
* - `true`: append a summary with default limits
156+
* - object form: append a summary with customized limits
154157
* @default true
155158
*/
156-
summary?: boolean;
159+
summary?:
160+
| boolean
161+
| {
162+
/**
163+
* Maximum characters for each failure message and diff, and each
164+
* unhandled error message and stack.
165+
* @default 400
166+
*/
167+
maxCharsPerField?: number;
168+
};
157169
};
158170

159171
export type BlobReporterOptions = {

packages/core/tests/reporter/githubActions.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,110 @@ describe('GithubActionsReporter step summary', () => {
214214
}
215215
});
216216

217+
it('uses the configured summary field length without changing the default', async () => {
218+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rstest-gha-'));
219+
const summaryPath = path.join(tempDir, 'summary.md');
220+
const defaultSummaryPath = path.join(tempDir, 'default-summary.md');
221+
const previousSummaryPath = process.env.GITHUB_STEP_SUMMARY;
222+
const previousWorkspacePath = process.env.GITHUB_WORKSPACE;
223+
const testPath = path.join(tempDir, 'tests/long-diff.test.ts');
224+
const diff = [
225+
'- Expected',
226+
'+ Received',
227+
'',
228+
'- expected first line',
229+
...Array.from(
230+
{ length: 40 },
231+
(_, index) => ` unchanged context line ${index}`,
232+
),
233+
'+ received last line',
234+
].join('\n');
235+
236+
process.env.GITHUB_STEP_SUMMARY = summaryPath;
237+
process.env.GITHUB_WORKSPACE = tempDir;
238+
239+
try {
240+
const reporter = new GithubActionsReporter({
241+
rootPath: tempDir,
242+
options: {
243+
onWritePath: (value) => value,
244+
annotations: false,
245+
summary: {
246+
maxCharsPerField: 2_000,
247+
},
248+
},
249+
});
250+
251+
const runEndPayload: Parameters<
252+
GithubActionsReporter['onTestRunEnd']
253+
>[0] = {
254+
results: [
255+
{
256+
testId: 'file-1',
257+
status: 'fail',
258+
name: 'long-diff.test.ts',
259+
testPath,
260+
project: 'rstest',
261+
results: [],
262+
},
263+
],
264+
testResults: [
265+
{
266+
testId: 'test-1',
267+
status: 'fail',
268+
name: 'shows the useful diff',
269+
parentNames: [],
270+
testPath,
271+
project: 'rstest',
272+
errors: [
273+
{
274+
name: 'AssertionError',
275+
message: 'values differ',
276+
diff,
277+
},
278+
],
279+
},
280+
],
281+
duration: emptyDuration,
282+
snapshotSummary: emptySnapshotSummary,
283+
getSourcemap: async () => null,
284+
};
285+
286+
await reporter.onTestRunEnd(runEndPayload);
287+
288+
process.env.GITHUB_STEP_SUMMARY = defaultSummaryPath;
289+
const defaultReporter = new GithubActionsReporter({
290+
rootPath: tempDir,
291+
options: {
292+
onWritePath: (value) => value,
293+
annotations: false,
294+
},
295+
});
296+
await defaultReporter.onTestRunEnd(runEndPayload);
297+
298+
const summary = await fs.readFile(summaryPath, 'utf-8');
299+
const defaultSummary = await fs.readFile(defaultSummaryPath, 'utf-8');
300+
expect(summary).toContain('- expected first line');
301+
expect(summary).toContain('+ received last line');
302+
expect(defaultSummary).toContain('- expected first line');
303+
expect(defaultSummary).not.toContain('+ received last line');
304+
} finally {
305+
if (previousSummaryPath === undefined) {
306+
delete process.env.GITHUB_STEP_SUMMARY;
307+
} else {
308+
process.env.GITHUB_STEP_SUMMARY = previousSummaryPath;
309+
}
310+
311+
if (previousWorkspacePath === undefined) {
312+
delete process.env.GITHUB_WORKSPACE;
313+
} else {
314+
process.env.GITHUB_WORKSPACE = previousWorkspacePath;
315+
}
316+
317+
await fs.rm(tempDir, { recursive: true, force: true });
318+
}
319+
});
320+
217321
it('renders flaky tests with a short summary of previous failures', async () => {
218322
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rstest-gha-'));
219323
const summaryPath = path.join(tempDir, 'summary.md');

0 commit comments

Comments
 (0)