Merge pull request #28310 from github/validate-versioning
Validate versioning
This commit is contained in:
@@ -7,7 +7,7 @@ redirect_from:
|
||||
- /actions/deployment/security-hardening-your-deployments/using-oidc-with-your-reusable-workflows
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: issue-4757-and-5856
|
||||
ghae: issue-4757
|
||||
ghec: '*'
|
||||
ghes: '>=3.5'
|
||||
type: how_to
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
shortTitle: Collapsed sections
|
||||
---
|
||||
## Creating a collapsed section
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
topics:
|
||||
- Issues
|
||||
- Pull requests
|
||||
|
||||
@@ -45,7 +45,7 @@ versions:
|
||||
|
||||
## Schema enforcement
|
||||
|
||||
The schema for validating the feature versioning lives in [`tests/helpers/schemas/feature-versions-schema.js`](/tests/helpers/schemas/feature-versions-schema.js) and is exercised by [`tests/linting/lint-files.js`](/tests/linting/lint-files.js).
|
||||
The schema for validating the feature versioning lives in [`tests/helpers/schemas/feature-versions-schema.js`](/tests/helpers/schemas/feature-versions-schema.js) and is exercised by [`tests/linting/lint-versioning.js`](/tests/linting/lint-versioning.js).
|
||||
|
||||
## Script to remove feature tags
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const plans = [
|
||||
releases: [latestNonNumberedRelease],
|
||||
latestRelease: latestNonNumberedRelease,
|
||||
nonEnterpriseDefault: true, // permanent way to refer to this plan if the name changes
|
||||
hasNumberedReleases: false,
|
||||
openApiBaseName: 'api.github.com', // used for REST
|
||||
miscBaseName: 'dotcom', // used for GraphQL and webhooks
|
||||
},
|
||||
@@ -25,6 +26,7 @@ const plans = [
|
||||
shortName: 'ghec',
|
||||
releases: [latestNonNumberedRelease],
|
||||
latestRelease: latestNonNumberedRelease,
|
||||
hasNumberedReleases: false,
|
||||
openApiBaseName: 'api.github.com',
|
||||
miscBaseName: 'ghec',
|
||||
},
|
||||
@@ -44,8 +46,11 @@ const plans = [
|
||||
shortName: 'ghae',
|
||||
releases: [latestNonNumberedRelease],
|
||||
latestRelease: latestNonNumberedRelease,
|
||||
hasNumberedReleases: false,
|
||||
openApiBaseName: 'github.ae',
|
||||
miscBaseName: 'ghae',
|
||||
allowedFrontmatterPattern: '^issue-\\d+?$',
|
||||
allowedInlinePattern: '^ghae-issue-\\d+?$',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -14,13 +14,6 @@ const layoutNames = [
|
||||
'release-notes',
|
||||
false,
|
||||
]
|
||||
const semverValidRange = semver.validRange
|
||||
const semverRange = {
|
||||
type: 'string',
|
||||
conform: semverValidRange,
|
||||
message: 'Must be a valid SemVer range',
|
||||
}
|
||||
const versionObjs = Object.values(allVersions)
|
||||
|
||||
const guideTypes = ['overview', 'quick_start', 'tutorial', 'how_to', 'reference']
|
||||
const featureVersions = fs
|
||||
@@ -246,19 +239,44 @@ const featureVersionsProp = {
|
||||
},
|
||||
}
|
||||
|
||||
const asteriskPattern = /^\*$/
|
||||
|
||||
schema.properties.versions = {
|
||||
type: ['object', 'string'], // allow a '*' string to indicate all versions
|
||||
required: true,
|
||||
properties: versionObjs.reduce((acc, versionObj) => {
|
||||
acc[versionObj.plan] = semverRange
|
||||
acc[versionObj.shortName] = semverRange
|
||||
additionalProperties: false, // don't allow any versions in FM that aren't defined in lib/all-versions
|
||||
properties: Object.values(allVersions).reduce((acc, versionObj) => {
|
||||
acc[versionObj.plan] = getValidProps(versionObj)
|
||||
acc[versionObj.shortName] = getValidProps(versionObj)
|
||||
return acc
|
||||
}, featureVersionsProp),
|
||||
}
|
||||
|
||||
// Support 'github-ae': next
|
||||
schema.properties.versions.properties['github-ae'] = 'next'
|
||||
schema.properties.versions.properties.ghae = 'next'
|
||||
function getValidProps(versionObj) {
|
||||
const valid = { type: 'string' }
|
||||
|
||||
// The properties attached to versionObj are defined in lib/all-versions.js.
|
||||
|
||||
// If a version has no numbered releases or exception pattern, the only valid value is '*'.
|
||||
if (!(versionObj.hasNumberedReleases || versionObj.allowedFrontmatterPattern)) {
|
||||
valid.pattern = asteriskPattern
|
||||
valid.message = `Must have a value of '*'`
|
||||
}
|
||||
|
||||
// If a version has an exception pattern, both '*' and the exception pattern are valid.
|
||||
if (versionObj.allowedFrontmatterPattern) {
|
||||
valid.pattern = new RegExp(`${asteriskPattern.source}|${versionObj.allowedFrontmatterPattern}`)
|
||||
valid.message = `Must have a value of '*' or 'issue-###', where ### is an integer`
|
||||
}
|
||||
|
||||
// If a version has numbered releases, any semver range is valid. Note '*' is a valid semver range.
|
||||
if (versionObj.hasNumberedReleases) {
|
||||
valid.conform = semver.validRange
|
||||
valid.message = 'Must be a valid SemVer range'
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
function frontmatter(markdown, opts = {}) {
|
||||
const defaults = {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Page versioned for next GitHub AE release
|
||||
versions:
|
||||
github-ae: 'next'
|
||||
---
|
||||
7
tests/fixtures/page-with-invalid-product-version.md
vendored
Normal file
7
tests/fixtures/page-with-invalid-product-version.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: This is an article
|
||||
intro: I have invalid versions frontmatter
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: 'issue-foo' # Only issue-<number> is allowed, per lib/all-versions.js
|
||||
---
|
||||
@@ -86,5 +86,14 @@ export default {
|
||||
description: 'final name used to map GraphQL and webhook schema names to the current version',
|
||||
type: 'string',
|
||||
},
|
||||
allowedFrontmatterPattern: {
|
||||
desciption: 'pattern used in a regex to validate versions frontmatter in lib/frontmatter.js',
|
||||
type: 'string',
|
||||
},
|
||||
allowedInlinePattern: {
|
||||
desciption:
|
||||
'pattern used in a regex to valid ifversion tag in tests/linting/lint-versioning.js',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -410,15 +410,13 @@ describe('lint markdown content', () => {
|
||||
isEarlyAccess,
|
||||
isSitePolicy,
|
||||
hasExperimentalAlternative,
|
||||
frontmatterErrors,
|
||||
frontmatterData
|
||||
|
||||
beforeAll(async () => {
|
||||
const fileContents = await fs.readFile(markdownAbsPath, 'utf8')
|
||||
const { data, content: bodyContent, errors } = frontmatter(fileContents)
|
||||
const { data, content: bodyContent } = frontmatter(fileContents)
|
||||
|
||||
content = bodyContent
|
||||
frontmatterErrors = errors
|
||||
frontmatterData = data
|
||||
ast = fromMarkdown(content)
|
||||
isHidden = data.hidden === true
|
||||
@@ -611,13 +609,6 @@ describe('lint markdown content', () => {
|
||||
})
|
||||
|
||||
if (!markdownRelPath.includes('data/reusables')) {
|
||||
test('contains valid frontmatter', () => {
|
||||
const errorMessage = frontmatterErrors
|
||||
.map((error) => `- [${error.property}]: ${error.actual}, ${error.message}`)
|
||||
.join('\n')
|
||||
expect(frontmatterErrors.length, errorMessage).toBe(0)
|
||||
})
|
||||
|
||||
test('frontmatter contains valid liquid', async () => {
|
||||
const fmKeysWithLiquid = ['title', 'shortTitle', 'intro', 'product', 'permission'].filter(
|
||||
(key) => Boolean(frontmatterData[key])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { jest } from '@jest/globals'
|
||||
import fs from 'fs/promises'
|
||||
import revalidator from 'revalidator'
|
||||
import semver from 'semver'
|
||||
import { allVersions } from '../../lib/all-versions.js'
|
||||
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'
|
||||
@@ -11,18 +11,20 @@ import walkFiles from '../../script/helpers/walk-files'
|
||||
import frontmatter from '../../lib/frontmatter.js'
|
||||
import loadSiteData from '../../lib/site-data.js'
|
||||
|
||||
const versionShortNames = Object.values(allVersions).map((v) => v.shortName)
|
||||
const versionShortNameExceptions = ['ghae-next', 'ghae-issue-']
|
||||
/*
|
||||
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('legacy')
|
||||
|
||||
const siteData = loadSiteData()
|
||||
const featureVersions = Object.entries(siteData.en.site.data.features)
|
||||
const featureVersionNames = featureVersions.map((fv) => fv[0])
|
||||
|
||||
const versionKeywords = versionShortNames
|
||||
.concat(['currentVersion', 'enterpriseServerReleases'])
|
||||
.concat(featureVersionNames)
|
||||
const allowedVersionNames = Object.keys(allVersionShortnames).concat(featureVersionNames)
|
||||
|
||||
// Make sure data/features/*.yml contains valid versioning.
|
||||
describe('lint feature versions', () => {
|
||||
@@ -79,7 +81,7 @@ describe('lint Liquid versioning', () => {
|
||||
// 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) =>
|
||||
versionKeywords.some((keyword) => cond.includes(keyword))
|
||||
allowedVersionNames.some((keyword) => cond.includes(keyword))
|
||||
)
|
||||
const errorMessage = `Found ${
|
||||
ifsForVersioning.length
|
||||
@@ -98,12 +100,16 @@ describe('lint Liquid versioning', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Return true if the shortname in the conditional is supported (fpt, ghec, ghes, ghae, all feature names).
|
||||
// If not, see if the shortname matches any exception pattern defined in lib/all-versions.js.
|
||||
function validateVersion(version) {
|
||||
return (
|
||||
versionShortNames.includes(version) ||
|
||||
versionShortNameExceptions.some((exception) => version.startsWith(exception)) ||
|
||||
featureVersionNames.includes(version)
|
||||
const isSupported = allowedVersionNames.includes(version)
|
||||
const isException = Object.values(allVersions).some(
|
||||
(v) => v.allowedInlinePattern && new RegExp(v.allowedInlinePattern).test(version)
|
||||
)
|
||||
const isValid = isSupported || isException
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
function validateIfversionConditionals(conds) {
|
||||
|
||||
@@ -297,24 +297,6 @@ describe('Page class', () => {
|
||||
return page.render(context)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test('support next GitHub AE version in frontmatter', async () => {
|
||||
// This fixture has `github-ae: 'next'` hardcoded in the frontmatter
|
||||
const page = await Page.init({
|
||||
relativePath: 'page-versioned-for-ghae-next.md',
|
||||
basePath: path.join(__dirname, '../fixtures'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
// set version to @latest
|
||||
const context = {
|
||||
currentVersion: 'github-ae@latest',
|
||||
currentLanguage: 'en',
|
||||
}
|
||||
context.currentPath = `/${context.currentLanguage}/${context.currentVersion}`
|
||||
await expect(() => {
|
||||
return page.render(context)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
test('preserves `languageCode`', async () => {
|
||||
@@ -794,6 +776,18 @@ describe('catches errors thrown in Page class', () => {
|
||||
await expect(getPage).rejects.toThrowError('versions')
|
||||
})
|
||||
|
||||
test('invalid versions frontmatter', async () => {
|
||||
async function getPage() {
|
||||
return await Page.init({
|
||||
relativePath: 'page-with-invalid-product-version.md',
|
||||
basePath: path.join(__dirname, '../fixtures'),
|
||||
languageCode: 'en',
|
||||
})
|
||||
}
|
||||
|
||||
await expect(getPage).rejects.toThrowError('versions')
|
||||
})
|
||||
|
||||
test('English page with a version in frontmatter that its parent product is not available in', async () => {
|
||||
async function getPage() {
|
||||
return await Page.init({
|
||||
|
||||
@@ -7,7 +7,7 @@ redirect_from:
|
||||
- /actions/deployment/security-hardening-your-deployments/using-oidc-with-your-reusable-workflows
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: issue-4757-and-5856
|
||||
ghae: issue-4757
|
||||
ghec: '*'
|
||||
ghes: '>=3.5'
|
||||
type: how_to
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
shortTitle: Secciones colapsadas
|
||||
---
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
topics:
|
||||
- Issues
|
||||
- Pull requests
|
||||
|
||||
@@ -7,7 +7,7 @@ redirect_from:
|
||||
- /actions/deployment/security-hardening-your-deployments/using-oidc-with-your-reusable-workflows
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: issue-4757-and-5856
|
||||
ghae: issue-4757
|
||||
ghec: '*'
|
||||
ghes: '>=3.5'
|
||||
type: how_to
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
shortTitle: Collapsed sections
|
||||
---
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
topics:
|
||||
- Issues
|
||||
- Pull requests
|
||||
|
||||
@@ -7,7 +7,7 @@ redirect_from:
|
||||
- /actions/deployment/security-hardening-your-deployments/using-oidc-with-your-reusable-workflows
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: issue-4757-and-5856
|
||||
ghae: issue-4757
|
||||
ghec: '*'
|
||||
ghes: '>=3.5'
|
||||
type: how_to
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
shortTitle: Seções colapsadas
|
||||
---
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
topics:
|
||||
- Issues
|
||||
- Pull requests
|
||||
|
||||
@@ -7,7 +7,7 @@ redirect_from:
|
||||
- /actions/deployment/security-hardening-your-deployments/using-oidc-with-your-reusable-workflows
|
||||
versions:
|
||||
fpt: '*'
|
||||
ghae: issue-4757-and-5856
|
||||
ghae: issue-4757
|
||||
ghec: '*'
|
||||
ghes: '>=3.5'
|
||||
type: how_to
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/organizing-information-with-collapsed-sections
|
||||
shortTitle: 折叠部分
|
||||
---
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ versions:
|
||||
ghes: '*'
|
||||
ghae: '*'
|
||||
ghec: '*'
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
redirect_from:
|
||||
- /github/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests
|
||||
topics:
|
||||
- Issues
|
||||
- Pull requests
|
||||
|
||||
Reference in New Issue
Block a user