Files
vercel__next.js/scripts/get-changed-tests.mjs
Sebastian "Sebbie" Silbermann 027205875b [ci] Run flake detection and new deploy tests when merged and on backport branches (#97991)
The previous detection mechanism special-cased `canary` which lead to
large diffs when backport branches ran CI.

We stop special casing `canary` reducing complexity and giving us
another flake-detecting attempt when the change is merged.

That way we can safely enable flake detection on backport branches
without having to test large diffs when PRs are merged targetting
backport branches. New backport branches will no longer have flake
detecton and new deploy test runs disabled.
2026-08-28 09:09:32 +00:00

218 lines
5.5 KiB
JavaScript

// @ts-check
import fs from 'fs/promises'
import execa from 'execa'
import { createRequire } from 'module'
import path from 'path'
import { getDiffRevision, getGitInfo } from './git-info.mjs'
const require = createRequire(import.meta.url)
const glob = require('glob')
const {
getTestFilterFromManifest,
mergeManifests,
} = require('../test/get-test-filter')
const DEFAULT_DEPLOY_TESTS_MANIFEST_PATH = 'test/deploy-tests-manifest.json'
const TEST_FILE_REGEX = /^test\/.*?\.test\.(js|ts|tsx)$/
function normalizePath(file) {
return file.replace(/\\/g, '/')
}
function getExcludedCases(suite = {}) {
return new Set([...(suite.failed ?? []), ...(suite.flakey ?? [])])
}
function getExternalTestsFilterPaths() {
const externalTestsFilters =
process.env.NEXT_EXTERNAL_TESTS_FILTERS ??
DEFAULT_DEPLOY_TESTS_MANIFEST_PATH
const manifestPaths = new Map()
for (const manifestPath of externalTestsFilters.split(',')) {
if (!manifestPath.trim()) {
continue
}
const absolutePath = path.resolve(process.cwd(), manifestPath)
const repoRelativePath = normalizePath(
path.relative(process.cwd(), absolutePath)
)
manifestPaths.set(repoRelativePath, {
absolutePath,
repoRelativePath,
})
}
return [...manifestPaths.values()]
}
function getDeployTestsFromManifest(manifest) {
const testFilter = getTestFilterFromManifest(manifest)
if (!testFilter) {
return []
}
const tests = glob
.sync('test/e2e/**/*.test.{js,ts,tsx}', {
cwd: process.cwd(),
ignore: '**/node_modules/**',
nodir: true,
})
.map((file) => ({ file: normalizePath(file), excludedCases: [] }))
return testFilter(tests).map((test) => test.file)
}
export function getDeployManifestChangedTests(
currentManifest,
previousManifest
) {
if (!currentManifest) {
return []
}
const previousVersion2Manifest = previousManifest ?? {
version: 2,
suites: {},
rules: { include: [], exclude: [] },
}
const previousIncludedTests = new Set(
getDeployTestsFromManifest(previousVersion2Manifest)
)
const changedTests = new Set()
for (const file of getDeployTestsFromManifest(currentManifest)) {
const currentExcludedCases = getExcludedCases(
currentManifest.suites?.[file]
)
const previousExcludedCases = getExcludedCases(
previousVersion2Manifest.suites?.[file]
)
if (!previousIncludedTests.has(file)) {
changedTests.add(file)
continue
}
for (const testCase of previousExcludedCases) {
if (!currentExcludedCases.has(testCase)) {
changedTests.add(file)
break
}
}
}
return [...changedTests]
}
/**
* Detects changed (see {@link getDiffRevision}) tests files.
* Returns tests separated by test mode (dev/prod), as well as the corresponding commit hash
* that the current branch is pointing to
*/
export default async function getChangedTests() {
/** @type import('execa').Options */
const EXECA_OPTS = { shell: true }
const { branchName, remoteUrl, commitSha } = await getGitInfo()
const diffRevision = await getDiffRevision()
const changesResult = await execa(
`git diff ${diffRevision} --name-only`,
EXECA_OPTS
).catch((err) => {
console.error(err)
return { stdout: '', stderr: '' }
})
console.log(
{
branchName,
remoteUrl,
commitSha,
},
`\ngit diff:\n${changesResult.stderr}\n${changesResult.stdout}`
)
const changedFiles = changesResult.stdout.split('\n')
// run each test 3 times in each test mode (if E2E) with no-retrying
// and if any fail it's flakey
const devTests = new Set()
const prodTests = new Set()
const deployTests = new Set()
for (let file of changedFiles) {
// normalize slashes
file = normalizePath(file)
const fileExists = await fs
.access(path.join(process.cwd(), file), fs.constants.F_OK)
.then(() => true)
.catch(() => false)
if (fileExists && file.match(TEST_FILE_REGEX)) {
if (file.startsWith('test/e2e/')) {
devTests.add(file)
prodTests.add(file)
deployTests.add(file)
} else if (file.startsWith('test/production')) {
prodTests.add(file)
} else if (file.startsWith('test/development')) {
devTests.add(file)
}
}
}
const externalTestsFilterPaths = getExternalTestsFilterPaths()
if (
externalTestsFilterPaths.some(({ repoRelativePath }) =>
changedFiles.includes(repoRelativePath)
)
) {
const currentManifest = mergeManifests(
await Promise.all(
externalTestsFilterPaths.map(async ({ absolutePath }) =>
JSON.parse(await fs.readFile(absolutePath, 'utf8'))
)
)
)
const previousManifest = mergeManifests(
(
await Promise.all(
externalTestsFilterPaths.map(async ({ repoRelativePath }) => {
const previousManifestOutput = await execa(
`git show ${diffRevision}:${repoRelativePath}`,
EXECA_OPTS
).catch(() => null)
return previousManifestOutput?.stdout
? JSON.parse(previousManifestOutput.stdout)
: null
})
)
).filter(Boolean)
)
for (const file of getDeployManifestChangedTests(
currentManifest,
previousManifest
)) {
deployTests.add(file)
}
}
const detectedTests = {
devTests: [...devTests],
prodTests: [...prodTests],
deployTests: [...deployTests],
}
console.log('Detected tests:', JSON.stringify(detectedTests, null, 2))
return { ...detectedTests, commitSha }
}