268 lines
9.8 KiB
JavaScript
Executable File
268 lines
9.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import fs from 'fs'
|
|
import walk from 'walk-sync'
|
|
import path from 'path'
|
|
import { escapeRegExp } from 'lodash-es'
|
|
import { Tokenizer } from 'liquidjs'
|
|
import frontmatter from '../../lib/read-frontmatter.js'
|
|
import { allVersions } from '../../lib/all-versions.js'
|
|
import { deprecated, oldestSupported } from '../../lib/enterprise-server-releases.js'
|
|
|
|
const allVersionKeys = Object.values(allVersions)
|
|
const dryRun = ['-d', '--dry-run'].includes(process.argv[2])
|
|
|
|
const walkFiles = (pathToWalk, ext) => {
|
|
return walk(path.posix.join(process.cwd(), pathToWalk), {
|
|
includeBasePath: true,
|
|
directories: false,
|
|
}).filter((file) => file.endsWith(ext) && !file.endsWith('README.md'))
|
|
}
|
|
|
|
const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md'))
|
|
const yamlFiles = walkFiles('data', '.yml')
|
|
|
|
const operatorsMap = {
|
|
// old: new
|
|
'==': '=',
|
|
ver_gt: '>',
|
|
ver_lt: '<',
|
|
'!=': '!=', // noop
|
|
}
|
|
|
|
// [start-readme]
|
|
//
|
|
// Run this script to convert long form Liquid conditionals (e.g., {% if currentVersion == "free-pro-team" %}) to
|
|
// the new custom tag (e.g., {% ifversion fpt %}) and also use the short names in versions frontmatter.
|
|
//
|
|
// [end-readme]
|
|
|
|
async function main() {
|
|
if (dryRun)
|
|
console.log('This is a dry run! The script will not write any files. Use for debugging.\n')
|
|
|
|
// 1. UPDATE MARKDOWN FILES (CONTENT AND REUSABLES)
|
|
console.log('Updating Liquid conditionals and versions frontmatter in Markdown files...\n')
|
|
for (const file of markdownFiles) {
|
|
// A. UPDATE LIQUID CONDITIONALS IN CONTENT
|
|
// Create an { old: new } conditionals object so we can get the replacements and
|
|
// make the replacements separately and not do both in nested loops.
|
|
const content = fs.readFileSync(file, 'utf8')
|
|
const contentReplacements = getLiquidReplacements(content, file)
|
|
const newContent = makeLiquidReplacements(contentReplacements, content)
|
|
|
|
// B. UPDATE FRONTMATTER VERSIONS PROPERTY
|
|
const { data } = frontmatter(newContent)
|
|
if (data.versions && typeof data.versions !== 'string') {
|
|
Object.entries(data.versions).forEach(([plan, value]) => {
|
|
// Update legacy versioning while we're here
|
|
const valueToUse = value
|
|
.replace('2.23', '3.0')
|
|
.replace(`>=${oldestSupported}`, '*')
|
|
.replace(/>=?2\.20/, '*')
|
|
.replace(/>=?2\.19/, '*')
|
|
|
|
// Find the relevant version from the master list so we can access the short name.
|
|
const versionObj = allVersionKeys.find(
|
|
(version) => version.plan === plan || version.shortName === plan,
|
|
)
|
|
if (!versionObj) {
|
|
console.error(`can't find supported version for ${plan}`)
|
|
process.exit(1)
|
|
}
|
|
delete data.versions[plan]
|
|
data.versions[versionObj.shortName] = valueToUse
|
|
})
|
|
}
|
|
|
|
if (dryRun) {
|
|
console.log(contentReplacements)
|
|
} else {
|
|
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 }))
|
|
}
|
|
}
|
|
|
|
// 2. UPDATE LIQUID CONDITIONALS IN DATA YAML FILES
|
|
console.log('Updating Liquid conditionals in YAML files...\n')
|
|
for (const file of yamlFiles) {
|
|
const yamlContent = fs.readFileSync(file, 'utf8')
|
|
const yamlReplacements = getLiquidReplacements(yamlContent, file)
|
|
// Update any `versions` properties in the YAML as well (learning tracks, etc.)
|
|
const newYamlContent = makeLiquidReplacements(yamlReplacements, yamlContent)
|
|
.replace(/("|')?free-pro-team("|')?:/g, 'fpt:')
|
|
.replace(/("|')?enterprise-server("|')?:/g, 'ghes:')
|
|
.replace(/("|')?github-ae("|')?:/g, 'ghae:')
|
|
|
|
if (dryRun) {
|
|
console.log(yamlReplacements)
|
|
} else {
|
|
fs.writeFileSync(file, newYamlContent)
|
|
}
|
|
}
|
|
}
|
|
|
|
main().then(
|
|
() => {
|
|
console.log('Done!')
|
|
},
|
|
(err) => {
|
|
console.error(err)
|
|
process.exit(1)
|
|
},
|
|
)
|
|
|
|
// Convenience function to help with readability by removing this large but unneded property.
|
|
function removeInputProps(arrayOfObjects) {
|
|
return arrayOfObjects.map((obj) => {
|
|
delete obj.input || delete obj.token.input
|
|
return obj
|
|
})
|
|
}
|
|
|
|
function makeLiquidReplacements(replacementsObj, text) {
|
|
let newText = text
|
|
Object.entries(replacementsObj).forEach(([oldCond, newCond]) => {
|
|
const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g')
|
|
newText = newText
|
|
.replace(oldCondRegex, `$1 ${newCond} $2`)
|
|
// Content files use an old-school hack to ensure our old regex deprecation script DTRT, for example:
|
|
// `if enterpriseServerVersions contains currentVersion and currentVersion ver_gt "enterprise-server@2.21"`
|
|
// This script will change the above to `if ghes and ghes > 2.21`.
|
|
// But we don't need the hack for the new deprecation script, because it will change `if ghes > 2.21` to `if ghes`.
|
|
// So we can update this to the simpler `{% if ghes > 2.21 %}`.
|
|
.replace(/ghes and ghes/g, 'ghes')
|
|
})
|
|
|
|
return newText
|
|
}
|
|
|
|
// Versions map:
|
|
// if currentVersion == "myVersion@myRelease" -> ifversion myVersionShort OR ifversion myVersionShort = @myRelease
|
|
// if currentVersion != "myVersion@myRelease" -> ifversion not myVersionShort OR ifversion myVersionShort != @myRelease
|
|
// if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease
|
|
// if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease
|
|
// if enterpriseServerVersions contains currentVersion -> ifversion ghes
|
|
function getLiquidReplacements(content, file) {
|
|
const replacements = {}
|
|
|
|
const tokenizer = new Tokenizer(content)
|
|
const tokens = removeInputProps(tokenizer.readTopLevelTokens())
|
|
|
|
tokens
|
|
.filter(
|
|
(token) =>
|
|
(token.name === 'if' || token.name === 'elsif') && token.content.includes('currentVersion'),
|
|
)
|
|
.map((token) => token.content)
|
|
.forEach((token) => {
|
|
const newToken = token.startsWith('if') ? ['ifversion'] : ['elsif']
|
|
// Everything from here on pushes to the `newToken` array to construct the new conditional.
|
|
token
|
|
.replace(/(if|elsif) /, '')
|
|
.split(/ (or|and) /)
|
|
.forEach((op) => {
|
|
if (op === 'or' || op === 'and') {
|
|
newToken.push(op)
|
|
return
|
|
}
|
|
|
|
// This string will always resolve to `ifversion ghes`.
|
|
if (op.includes('enterpriseServerVersions contains currentVersion')) {
|
|
newToken.push('ghes')
|
|
return
|
|
}
|
|
|
|
// For the rest, we need to check the release string.
|
|
|
|
// E.g., [ 'currentVersion', '==', '"enterprise-server@3.0"'].
|
|
const opParts = op.split(' ')
|
|
|
|
if (!(opParts.length === 3 && opParts[0] === 'currentVersion')) {
|
|
console.error(`Something went wrong with ${token} in ${file}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const operator = opParts[1]
|
|
// Remove quotes around the version and then split it on the at sign.
|
|
const [plan, release] = opParts[2].slice(1, -1).split('@')
|
|
|
|
// Find the relevant version from the master list so we can access the short name.
|
|
const versionObj = allVersionKeys.find((version) => version.plan === plan)
|
|
|
|
if (!versionObj) {
|
|
console.error(`Couldn't find a version for ${plan} in "${token}" in ${file}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Handle numbered releases!
|
|
if (versionObj.hasNumberedReleases) {
|
|
const newOperator = operatorsMap[operator]
|
|
if (!newOperator) {
|
|
console.error(
|
|
`Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`,
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Account for this one weird version included in a couple content files
|
|
deprecated.push('1.19')
|
|
|
|
// E.g., ghes > 2.20
|
|
const availableInAllGhes = deprecated.includes(release) && newOperator === '>'
|
|
|
|
// We can change > deprecated releases, like ghes > 2.19, to just ghes.
|
|
// These are now available for all ghes releases.
|
|
if (availableInAllGhes) {
|
|
newToken.push(versionObj.shortName)
|
|
return
|
|
}
|
|
|
|
// E.g., ghes < 2.20
|
|
const lessThanDeprecated = deprecated.includes(release) && newOperator === '<'
|
|
// E.g., ghes < 2.21
|
|
const lessThanOldestSupported = release === oldestSupported && newOperator === '<'
|
|
// E.g., ghes = 2.20
|
|
const equalsDeprecated = deprecated.includes(release) && newOperator === '='
|
|
const hasDeprecatedContent =
|
|
lessThanDeprecated || lessThanOldestSupported || equalsDeprecated
|
|
|
|
// Remove these by hand.
|
|
if (hasDeprecatedContent) {
|
|
console.error(`Found content that needs to be removed! See "${token} in "${file}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Override for legacy 2.23, which should be 3.0
|
|
const releaseToUse = release === '2.23' ? '3.0' : release
|
|
|
|
newToken.push(`${versionObj.shortName} ${newOperator} ${releaseToUse}`)
|
|
return
|
|
}
|
|
|
|
// Turn != into nots, now that we can assume this is not a numbered release.
|
|
if (operator === '!=') {
|
|
newToken.push(`not ${versionObj.shortName}`)
|
|
return
|
|
}
|
|
|
|
// We should only have equality conditionals left.
|
|
if (operator !== '==') {
|
|
console.error(`Expected == but found ${operator} in "${op}" in ${token}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Handle `latest`!
|
|
if (release === 'latest') {
|
|
newToken.push(versionObj.shortName)
|
|
return
|
|
}
|
|
|
|
// Handle all other non-standard releases, like github-ae@next and github-ae@issue-12345
|
|
newToken.push(`${versionObj.shortName}-${release}`)
|
|
})
|
|
|
|
replacements[token] = newToken.join(' ')
|
|
})
|
|
|
|
return replacements
|
|
}
|