зеркало из https://github.com/github/docs.git
234 строки
9.8 KiB
JavaScript
234 строки
9.8 KiB
JavaScript
import { jest } from '@jest/globals'
|
|
import fs from 'fs/promises'
|
|
import revalidator from 'revalidator'
|
|
import semver from 'semver'
|
|
import { allVersions, allVersionShortnames } from '../../lib/all-versions.js'
|
|
import { supported, next, nextNext, deprecated } from '../../lib/enterprise-server-releases.js'
|
|
import { getLiquidConditionals } from '../../script/helpers/get-liquid-conditionals.js'
|
|
import allowedVersionOperators from '../../lib/liquid-tags/ifversion-supported-operators.js'
|
|
import featureVersionsSchema from '../helpers/schemas/feature-versions-schema.js'
|
|
import walkFiles from '../../script/helpers/walk-files'
|
|
import frontmatter from '../../lib/frontmatter.js'
|
|
import cleanUpDeprecatedGhaeFlagErrors from '../../lib/temporary-ghae-deprecated-flag-error-cleanup.js'
|
|
import { getDeepDataByLanguage } from '../../lib/get-data.js'
|
|
|
|
/*
|
|
NOTE: This test suite does NOT validate the `versions` frontmatter in content files.
|
|
That's because lib/page.js validates frontmatter when loading all the pages (which happens
|
|
when running npm start or tests) and throws an error immediately if there are any issues.
|
|
This test suite DOES validate the data/features `versions` according to the same FM schema.
|
|
Some tests/unit/page.js tests also exercise the frontmatter validation.
|
|
*/
|
|
|
|
jest.useFakeTimers({ legacyFakeTimers: true })
|
|
|
|
const featureVersions = Object.entries(getDeepDataByLanguage('features', 'en'))
|
|
const featureVersionNames = featureVersions.map((fv) => fv[0])
|
|
const allowedVersionNames = Object.keys(allVersionShortnames).concat(featureVersionNames)
|
|
|
|
// Make sure data/features/*.yml contains valid versioning.
|
|
describe('lint feature versions', () => {
|
|
test.each(featureVersions)('data/features/%s matches the schema', (name, featureVersion) => {
|
|
let { errors } = revalidator.validate(featureVersion, featureVersionsSchema)
|
|
|
|
// TODO temporary kludge! See notes in the module.
|
|
if (errors.length) {
|
|
errors = cleanUpDeprecatedGhaeFlagErrors(errors)
|
|
}
|
|
|
|
const errorMessage = errors
|
|
.map((error) => {
|
|
// Make this one message a little more readable than the error we get from revalidator
|
|
// when additionalProperties is set to false and an additional prop is found.
|
|
const errorToReport =
|
|
error.message === 'must not exist' && error.actual.feature
|
|
? `feature: '${error.actual.feature}'`
|
|
: JSON.stringify(error.actual, null, 2)
|
|
|
|
return `- [${error.property}]: ${errorToReport}, ${error.message}`
|
|
})
|
|
.join('\n')
|
|
|
|
expect(errors.length, errorMessage).toBe(0)
|
|
})
|
|
})
|
|
|
|
const allFiles = walkFiles('content', '.md').concat(walkFiles('data', ['.yml', '.md']))
|
|
|
|
// Quoted strings in Liquid, like {% if "foo" %}, will always evaluate true _because_ they are strings.
|
|
// Instead we need to use unquoted variables, like {% if foo %}.
|
|
const stringInLiquidRegex = /{% (?:if|ifversion|elseif|unless) (?:"|').+?%}/g
|
|
|
|
// Make sure the `if` and `ifversion` Liquid tags in content and data files are valid.
|
|
describe('lint Liquid versioning', () => {
|
|
describe.each(allFiles)('%s', (file) => {
|
|
let fileContents, ifversionConditionals, ifConditionals
|
|
|
|
beforeAll(async () => {
|
|
fileContents = await fs.readFile(file, 'utf8')
|
|
const { data, content: bodyContent } = frontmatter(fileContents)
|
|
|
|
ifversionConditionals = getLiquidConditionals(data, ['ifversion', 'elsif']).concat(
|
|
getLiquidConditionals(bodyContent, ['ifversion', 'elsif'])
|
|
)
|
|
|
|
ifConditionals = getLiquidConditionals(data, 'if').concat(
|
|
getLiquidConditionals(bodyContent, 'if')
|
|
)
|
|
})
|
|
|
|
// `ifversion` supports both standard and feature-based versioning.
|
|
test('ifversion conditionals are valid', async () => {
|
|
const errors = validateIfversionConditionals(ifversionConditionals)
|
|
expect(errors.length, errors.join('\n')).toBe(0)
|
|
})
|
|
|
|
// Now that `ifversion` supports feature-based versioning, we should have few other `if` tags.
|
|
test('ifversion, not if, is used for versioning', async () => {
|
|
const ifsForVersioning = ifConditionals.filter((cond) =>
|
|
allowedVersionNames.some((keyword) => cond.includes(keyword))
|
|
)
|
|
const errorMessage = `Found ${
|
|
ifsForVersioning.length
|
|
} "if" conditionals used for versioning! Use "ifversion" instead.
|
|
${ifsForVersioning.join('\n')}`
|
|
expect(ifsForVersioning.length, errorMessage).toBe(0)
|
|
})
|
|
|
|
test('does not contain Liquid that evaluates strings (because they are always true)', async () => {
|
|
const matches = fileContents.match(stringInLiquidRegex) || []
|
|
const message =
|
|
'Found Liquid conditionals that evaluate a string instead of a variable. Remove the quotes around the variable!'
|
|
const errorMessage = `${message}\n - ${matches.join('\n - ')}`
|
|
expect(matches.length, errorMessage).toBe(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
// Return true if the shortname in the conditional is supported (fpt, ghec, ghes, ghae, all feature names).
|
|
function validateVersion(version) {
|
|
return (
|
|
allowedVersionNames.includes(version) ||
|
|
// TODO - REMOVE THE FOLLOWING 'OR' WHEN GHAE IS UPDATED WITH SEMVER VERSIONING
|
|
/ghae-issue-\d{4}/.test(version)
|
|
)
|
|
}
|
|
|
|
// TODO: Temporary check for presence of deprecated GHAE feature flags in FM.
|
|
// See details in docs-internal#29178.
|
|
// We can remove this after semantic versioning has been in place for a while.
|
|
function checkForDeprecatedGhaeVersioning(version, errors) {
|
|
if (/ghae-issue-\d+/.test(version)) {
|
|
errors.push(`
|
|
Lightweight feature flags ('${version}') are no longer supported in content. Use semantic versioning instead (ghae > 3.x or ghae: '> 3.x').
|
|
`)
|
|
}
|
|
}
|
|
|
|
function validateIfversionConditionals(conds) {
|
|
const errors = []
|
|
|
|
conds.forEach((cond) => {
|
|
// Where `cond` is an array of strings, where each string may have one of the following space-separated formats:
|
|
// * Length 1: `<version>` (example: `fpt`)
|
|
// * Length 2: `not <version>` (example: `not ghae`)
|
|
// * Length 3: `<version> <operator> <release>` (example: `ghes > 3.0`)
|
|
//
|
|
// Note that Lengths 1 and 2 may be used with feature-based versioning, but NOT Length 3.
|
|
const condParts = cond.split(/ (or|and) /).filter((part) => !(part === 'or' || part === 'and'))
|
|
|
|
condParts.forEach((str) => {
|
|
const strParts = str.split(' ')
|
|
// if length = 1, this should be a valid short version or feature version name.
|
|
if (strParts.length === 1) {
|
|
const version = strParts[0]
|
|
// TODO: This is temporary, see comment on the function.
|
|
checkForDeprecatedGhaeVersioning(version, errors)
|
|
// END TODO.
|
|
const isValidVersion = validateVersion(version)
|
|
if (!isValidVersion) {
|
|
errors.push(`"${version}" is not a valid short version or feature version name`)
|
|
}
|
|
}
|
|
|
|
// if length = 2, this should be 'not' followed by a valid short version name.
|
|
if (strParts.length === 2) {
|
|
const [notKeyword, version] = strParts
|
|
// TODO: This is temporary, see comment on the function.
|
|
checkForDeprecatedGhaeVersioning(version, errors)
|
|
// END TODO.
|
|
const isValidVersion = validateVersion(version)
|
|
const isValid = notKeyword === 'not' && isValidVersion
|
|
if (!isValid) {
|
|
errors.push(`"${cond}" is not a valid conditional`)
|
|
}
|
|
}
|
|
|
|
// if length = 3, this should be a range in the format: ghes > 3.0
|
|
// where the first item is `ghes` (currently the only version with numbered releases),
|
|
// the second item is a supported operator, and the third is a supported GHES release.
|
|
if (strParts.length === 3) {
|
|
const [version, operator, release] = strParts
|
|
const hasSemanticVersioning = Object.values(allVersions).some(
|
|
(v) => (v.hasNumberedReleases || v.internalLatestRelease) && v.shortName === version
|
|
)
|
|
if (!hasSemanticVersioning) {
|
|
errors.push(
|
|
`Found "${version}" inside "${cond}" with a "${operator}" operator, but "${version}" does not support semantic comparisons"`
|
|
)
|
|
}
|
|
if (!allowedVersionOperators.includes(operator)) {
|
|
errors.push(
|
|
`Found a "${operator}" operator inside "${cond}", but "${operator}" is not supported`
|
|
)
|
|
}
|
|
// Check nextNext is one version ahead of next
|
|
if (!isNextVersion(next, nextNext)) {
|
|
errors.push(
|
|
`The nextNext version: "${nextNext} is not one version ahead of the next supported version: "${next}" - check lib/enterprise-server-releases.js`
|
|
)
|
|
}
|
|
// Check that the versions in conditionals are supported
|
|
// versions of GHES or the first deprecated version. Allowing
|
|
// the first deprecated version to exist in code ensures
|
|
// allows us to deprecate the version before removing
|
|
// the old liquid content.
|
|
if (
|
|
!(
|
|
supported.includes(release) ||
|
|
release === next ||
|
|
release === nextNext ||
|
|
deprecated[0] === release
|
|
)
|
|
) {
|
|
errors.push(
|
|
`Found ${release} inside "${cond}", but ${release} is not a supported GHES release`
|
|
)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
return errors
|
|
}
|
|
|
|
function isNextVersion(v1, v2) {
|
|
const semverNext = semver.coerce(v1)
|
|
const semverNextNext = semver.coerce(v2)
|
|
const semverSupported = []
|
|
|
|
supported.forEach((el, i) => {
|
|
semverSupported[i] = semver.coerce(el)
|
|
})
|
|
// Check that the next version is the next version from the supported list first
|
|
const maxVersion = semver.maxSatisfying(semverSupported, '*').raw
|
|
const nextVersionCheck =
|
|
semverNext.raw === semver.inc(maxVersion, 'minor') ||
|
|
semverNext.raw === semver.inc(maxVersion, 'major')
|
|
return (
|
|
nextVersionCheck &&
|
|
(semver.inc(semverNext, 'minor') === semverNextNext.raw ||
|
|
semver.inc(semverNext, 'major') === semverNextNext.raw)
|
|
)
|
|
}
|