Repository: facebook/jest
File: packages/jest-reporters/src/GitHubActionsReporter.ts (TypeScript)

Code before:
 */

import {relative} from 'path';
import slash = require('slash');
import stripAnsi = require('strip-ansi');
import type {Test, TestCaseResult} from '@jest/test-result';
import {
  getStackTraceLines,
  getTopFrame,
  separateMessageFromStack,
} from 'jest-message-util';
import BaseReporter from './BaseReporter';

const errorTitleSeparator = ' \u203A ';

export default class GitHubActionsReporter extends BaseReporter {
  static readonly filename = __filename;

  override onTestCaseResult(
    test: Test,
    {failureMessages, ancestorTitles, title}: TestCaseResult,
  ): void {
    failureMessages.forEach(failure => {
      const {message, stack} = separateMessageFromStack(stripAnsi(failure));

      const relativeTestPath = slash(relative('', test.path));
      const stackLines = getStackTraceLines(stack);
      const normalizedStackLines = stackLines.map(line =>
        line.replace(test.path, relativeTestPath),
      );

      const topFrame = getTopFrame(stackLines);

      const errorTitle = [...ancestorTitles, title].join(errorTitleSeparator);
      const errorMessage = normalizeMessage(
        [message, ...normalizedStackLines].join('\n'),
      );

      this.log(
        `\n::error file=${test.path},line=${topFrame?.line},title=${errorTitle}::${errorMessage}`,
      );
    });
  }
}

// copied from: https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts
function normalizeMessage(input: string): string {
  return input.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
}


Review comment (suggestion):
For some reason, `rootDir` is not pointing to the right place. See https://github.com/facebook/jest/pull/12822#issuecomment-1120434301.

In the other hand, in CI it feels right to keep paths relative to root of the repo.

Code after:
 */

import stripAnsi = require('strip-ansi');
import type {Test, TestResult} from '@jest/test-result';
import type {Config} from '@jest/types';
import {
  formatPath,
  getStackTraceLines,
  getTopFrame,
  separateMessageFromStack,
} from 'jest-message-util';
import BaseReporter from './BaseReporter';

type AnnotationOptions = {
  file?: string;
  line?: number | string;
  message: string;
  title: string;
  type: 'error' | 'warning';
};

const titleSeparator = ' \u203A ';

export default class GitHubActionsReporter extends BaseReporter {
  static readonly filename = __filename;

  onTestFileResult({context}: Test, {testResults}: TestResult): void {
    testResults.forEach(result => {
      const title = [...result.ancestorTitles, result.title].join(
        titleSeparator,
      );

      result.retryReasons?.forEach((retryReason, index) => {
        this.#createAnnotation({
          ...this.#getMessageDetails(retryReason, context.config),
          title: `RETRY ${index + 1}: ${title}`,
          type: 'warning',
        });
      });

      result.failureMessages.forEach(failureMessage => {
        this.#createAnnotation({
          ...this.#getMessageDetails(failureMessage, context.config),
          title,
          type: 'error',
        });
      });
    });
  }

  #getMessageDetails(failureMessage: string, config: Config.ProjectConfig) {