diff --git a/bin/github.js b/bin/github.js new file mode 100644 index 0000000..477baa9 --- /dev/null +++ b/bin/github.js @@ -0,0 +1,12 @@ +import { relative, resolve, sep } from 'node:path' + +// Path used for the GitHub `::error file=` annotation. GitHub resolves it from the repo root +// (GITHUB_WORKSPACE), but under lerna/nx the cwd is the package dir, so a cwd-relative path won't +// map onto the PR diff. Anchor to GITHUB_WORKSPACE in CI so the annotation lands on the right file. +// GitHub matches annotation paths with POSIX separators, so emit forward slashes even on Windows +// runners (where node:path would otherwise produce backslashes that never match the repo file). +export const resolveAnnotationFile = (rawFile, { cwd, CI, GITHUB_WORKSPACE }) => { + const absolute = resolve(cwd, rawFile) + const base = CI && GITHUB_WORKSPACE ? GITHUB_WORKSPACE : cwd + return relative(base, absolute).split(sep).join('/') +} diff --git a/bin/reporter.js b/bin/reporter.js index f7eb8b5..c035351 100644 --- a/bin/reporter.js +++ b/bin/reporter.js @@ -4,6 +4,7 @@ import { relative, resolve, normalize } from 'node:path' import { spec as SpecReporter } from 'node:test/reporters' import { fileURLToPath } from 'node:url' import { color, haveColors, dim } from './color.js' +import { resolveAnnotationFile } from './github.js' const { CI, GITHUB_WORKSPACE, LERNA_PACKAGE_NAME } = process.env @@ -159,7 +160,12 @@ export default async function nodeTestReporterExodus(source) { if (path.length > 0) assert(path.pop() === data.name) // afterAll can generate failures too, with an empty path if (!data.todo) failedFiles.add(file) if (!notPrintedError(data.details.error)) { - const { body, loc } = extractError(data, relative(cwd, data.file || data.name)) // might be different from current file if in subimport + const errorFile = resolveAnnotationFile(data.file || data.name, { + cwd, + CI, + GITHUB_WORKSPACE, + }) + const { body, loc } = extractError(data, errorFile) // might be different from current file if in subimport if (!data.todo && CI && loc.line != null && loc.col != null) { print(`::error ${serializeGitHub(Object.entries(loc))}::${escapeGitHub(body)}`) } else if (body) { diff --git a/package.json b/package.json index 15d66bf..98e7f0f 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "bin/electron.js", "bin/electron.preload.cjs", "bin/find-binary.js", + "bin/github.js", "bin/inband.js", "bin/reporter.js", "loader/babel.cjs", diff --git a/tests/github.test.js b/tests/github.test.js new file mode 100644 index 0000000..678d6c7 --- /dev/null +++ b/tests/github.test.js @@ -0,0 +1,31 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { resolveAnnotationFile } from '../bin/github.js' + +// GitHub resolves `::error file=...` annotation paths from the repo root (GITHUB_WORKSPACE). +// lerna/nx run each package with cwd = the package dir, so a cwd-relative path points at the wrong +// place and the annotation never lands on the PR diff. In CI the path must be workspace-relative. +const GITHUB_WORKSPACE = '/home/runner/work/repo/repo' +const cwd = `${GITHUB_WORKSPACE}/packages/foo` // what lerna/nx hands the reporter +const absFile = `${cwd}/src/bar.test.js` + +test('CI annotation path is repo-root-relative so it lands on the PR diff', () => { + const file = resolveAnnotationFile(absFile, { cwd, CI: '1', GITHUB_WORKSPACE }) + assert.equal(file, 'packages/foo/src/bar.test.js') +}) + +test('a relative input is resolved against cwd before anchoring to the workspace', () => { + const file = resolveAnnotationFile('src/bar.test.js', { cwd, CI: '1', GITHUB_WORKSPACE }) + assert.equal(file, 'packages/foo/src/bar.test.js') +}) + +test('outside CI the path stays cwd-relative (local runs are unchanged)', () => { + assert.equal( + resolveAnnotationFile(absFile, { cwd, CI: undefined, GITHUB_WORKSPACE }), + 'src/bar.test.js' + ) + assert.equal( + resolveAnnotationFile(absFile, { cwd, CI: '1', GITHUB_WORKSPACE: undefined }), + 'src/bar.test.js' + ) +})