зеркало из https://github.com/github/docs.git
314 строки
9.8 KiB
JavaScript
Executable File
314 строки
9.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { program } from 'commander'
|
|
import walk from 'walk-sync'
|
|
import { execSync } from 'child_process'
|
|
|
|
import frontmatter from '../lib/read-frontmatter.js'
|
|
|
|
const scriptName = new URL(import.meta.url).pathname.split('/').pop()
|
|
|
|
const contentDir = path.resolve('content')
|
|
const reusablesDir = path.resolve('data/reusables')
|
|
const dataDir = path.resolve('data')
|
|
|
|
const currentBranch = execSync('git branch --show-current').toString().trim()
|
|
const plan = 'ghae'
|
|
|
|
// [start-readme]
|
|
//
|
|
// Find and replace lightweight feature flags for GitHub AE content.
|
|
//
|
|
// [end-readme]
|
|
|
|
program
|
|
.description(
|
|
'Toggle issue-based, feature-flagged versioning for GitHub AE content like\n' +
|
|
'ghae-issue-1234, then commit the results.\n\n' +
|
|
'Documentation: https://github.com/github/docs-content/blob/main/docs-content-docs/docs-content-workflows/content-creation/versioning-documentation.md#internal-versioning-conventions-for-github-ae\n\n' +
|
|
'Examples:\n' +
|
|
` ${scriptName} -f 'issue-1234, issue-5678'\n` +
|
|
` ${scriptName} -f 'issue-1234' -c '3.5'`
|
|
)
|
|
.option('-s, --show-flags', 'show list of existing flags')
|
|
.option("-f, --toggle-flags '<flag-1>[,flag-2,...]'", 'toggle comma-separated list of flags')
|
|
.option("-c, --comparative-replacement '<version>'", 'convert flags to comparative versioning')
|
|
.parse(process.argv)
|
|
|
|
const options = program.opts()
|
|
|
|
let optionsCount = 0
|
|
options.showFlags && optionsCount++
|
|
options.toggleFlags && optionsCount++
|
|
options.comparativeReplacement && optionsCount++
|
|
|
|
// Handle options:
|
|
// - Show help for no options
|
|
// - Indicate that --comparative-replacement requires --toggle-flags
|
|
// - Otherwise, if no --comparative-replacement, error when more than one
|
|
|
|
if (optionsCount === 0) {
|
|
program.help()
|
|
} else {
|
|
if (options.comparativeReplacement && !options.toggleFlags) {
|
|
console.log(`Error: --comparative-replacement requires --toggle-flags`)
|
|
process.exit(1)
|
|
} else if (optionsCount > 1 && options.showFlags) {
|
|
console.log(`Error: you specified ${optionsCount} options (accepts 1 or 2)`)
|
|
process.exit(1)
|
|
} else if (optionsCount > 2) {
|
|
console.log(`Error: you specified ${optionsCount} options (accepts 1 or 2)`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// Store flags that user wants to toggle.
|
|
|
|
let flagsToToggle = []
|
|
let flagCount = 0
|
|
if (options.toggleFlags) {
|
|
flagsToToggle = options.toggleFlags.split(',').map((item) => item.trim())
|
|
flagCount = flagsToToggle.length
|
|
}
|
|
|
|
if (options.toggleFlags) {
|
|
// Refuse to proceed if repository has uncommitted changes.
|
|
|
|
const localChangesCount = execSync(
|
|
`git status ${contentDir} ${reusablesDir} ${dataDir} --porcelain=v1 2>/dev/null | wc -l`
|
|
).toString()
|
|
|
|
if (localChangesCount > 0) {
|
|
console.log("Error: refusing to proceed due to uncommitted changes (review 'git status')")
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// Evaluate and store version number for comparative replacement, or abort.
|
|
|
|
if (options.comparativeReplacement) {
|
|
if (!options.comparativeReplacement.match(/\d\.\d+/)) {
|
|
console.log(
|
|
`Error: you specified ${options.comparativeReplacement} for comparative replacement (must be #.# or #.##)`
|
|
)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// Set replacement values for YAML and Liquid.
|
|
|
|
let replacementYAMLValue = '*'
|
|
let replacementLiquidValue = plan
|
|
|
|
if (options.comparativeReplacement) {
|
|
replacementYAMLValue = `>= ${options.comparativeReplacement}`
|
|
|
|
const liquidVersion = options.comparativeReplacement.split('.', 2)
|
|
if (liquidVersion[1] === '0') {
|
|
console.log(
|
|
`Error: you specified ${options.comparativeReplacement} for comparative replacement (can't be #.0)`
|
|
)
|
|
process.exit(1)
|
|
} else {
|
|
liquidVersion[1] = liquidVersion[1] - 1
|
|
const liquidVersionForComparison = liquidVersion.join('.')
|
|
replacementLiquidValue = `${plan} > ${liquidVersionForComparison}`
|
|
}
|
|
}
|
|
|
|
// Gather the files.
|
|
|
|
const markdownFiles = walk(contentDir, { includeBasePath: true, directories: false })
|
|
.concat(walk(reusablesDir, { includeBasePath: true, directories: false }))
|
|
.filter((file) => file.endsWith('.md') && !/readme\.md/i.test(file))
|
|
|
|
const yamlFiles = walk(dataDir, {
|
|
includeBasePath: true,
|
|
directories: false,
|
|
ignore: ['graphql'],
|
|
}).filter((file) => file.endsWith('.yml'))
|
|
|
|
const allFiles = [...markdownFiles, ...yamlFiles]
|
|
|
|
// --------------------------------------------------------------- --show-flags
|
|
|
|
// Create a dictionary to populate with keys as flag names that reference
|
|
// arrays of paths to files that contain the flag in YAML front matter or
|
|
// Liquid conditionals.
|
|
|
|
const allFlags = {}
|
|
const liquidShowRegExp = new RegExp(`\\s+${plan}-([^\\s]+)`, 'g')
|
|
|
|
console.log(`Parsing all flags for ${plan} plan on ${currentBranch} branch...`)
|
|
|
|
allFiles.forEach((file) => {
|
|
const fileContent = fs.readFileSync(file, 'utf8')
|
|
const matches = []
|
|
|
|
if (file.endsWith('.md')) {
|
|
const { data } = frontmatter(fileContent)
|
|
|
|
// Process YAML front matter.
|
|
|
|
if (data.versions && data.versions[plan] && data.versions[plan] !== '*') {
|
|
if (!allFlags[data.versions[plan]]) {
|
|
allFlags[data.versions[plan]] = []
|
|
}
|
|
|
|
allFlags[data.versions[plan]].push(file)
|
|
}
|
|
|
|
// Process Liquid conditionals in Markdown source.
|
|
|
|
const deduplicatedMatches = [...new Set(fileContent.match(liquidShowRegExp))]
|
|
|
|
if (deduplicatedMatches.length > 0) {
|
|
matches.push(...deduplicatedMatches.map((match) => match.trim().replace(plan + '-', '')))
|
|
}
|
|
} else if (file.endsWith('.yml')) {
|
|
// Process versions in YAML files for feature-based versioning.
|
|
|
|
const yamlShowRegExp = new RegExp(`${plan}: ['"]([A-Za-z0-9-_]+)['"]`, 'g')
|
|
const deduplicatedLiquidMatches = [...new Set(fileContent.match(yamlShowRegExp))]
|
|
const deduplicatedVersionMatches = [...new Set(fileContent.match(liquidShowRegExp))]
|
|
|
|
if (deduplicatedLiquidMatches.length > 0) {
|
|
matches.push(
|
|
...deduplicatedLiquidMatches.map((match) =>
|
|
match.trim().replace(`${plan}: `, '').replace(/['"]+/g, '')
|
|
)
|
|
)
|
|
}
|
|
|
|
if (deduplicatedVersionMatches.length > 0) {
|
|
matches.push(
|
|
...deduplicatedVersionMatches.map((match) => match.trim().replace(plan + '-', ''))
|
|
)
|
|
}
|
|
} else {
|
|
throw new Error(`Unrecognized file (${file}). Not a .md or .yml file.`)
|
|
}
|
|
|
|
matches.forEach((match) => {
|
|
if (!allFlags[match]) {
|
|
allFlags[match] = []
|
|
}
|
|
allFlags[match].push(file)
|
|
})
|
|
})
|
|
|
|
// Output flags and lists of files that contain the flags.
|
|
|
|
if (options.showFlags) {
|
|
let flag, files
|
|
for ([flag, files] of Object.entries(allFlags)) {
|
|
if (flag.match(/^issue-[0-9]+$/)) {
|
|
console.log(
|
|
`\n🚩 \x1b[7m ${plan}-${flag} \x1b[0m \x1b[1m\x1b[34m\x1b[4m${flag.replace(
|
|
'issue-',
|
|
'https://github.com/github/docs-content/issues/'
|
|
)}\x1b[0m`
|
|
)
|
|
} else {
|
|
console.log(`\n🚩 \x1b[43m ${plan}-${flag} \x1b[0m`)
|
|
}
|
|
|
|
files.forEach((file) => {
|
|
console.log(` ${file}`)
|
|
})
|
|
}
|
|
|
|
// ------------------------------------------------------------- --toggle-flags
|
|
} else if (options.toggleFlags) {
|
|
let commitCount = 0
|
|
|
|
console.log(`Toggling flag${flagCount > 1 ? 's' : ''} (${flagsToToggle.join(', ')})...`)
|
|
|
|
flagsToToggle.forEach((flag) => {
|
|
if (!(flag in allFlags)) {
|
|
console.warn(`${flag} does not exist in source`)
|
|
return
|
|
}
|
|
|
|
allFlags[flag].forEach((file) => {
|
|
const fileContent = fs.readFileSync(file, 'utf8')
|
|
const { data } = frontmatter(fileContent)
|
|
const liquidReplacementRegExp = new RegExp(`${plan}-${flag}`, 'g')
|
|
let newContent
|
|
|
|
if (file.endsWith('.md')) {
|
|
// Update versions in Liquid conditionals.
|
|
|
|
newContent = fileContent.replace(liquidReplacementRegExp, replacementLiquidValue)
|
|
|
|
if (data.versions && data.versions[plan] === flag) {
|
|
// Update versions in content files with YAML front matter.
|
|
|
|
data.versions[plan] = replacementYAMLValue
|
|
}
|
|
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 }))
|
|
} else if (file.endsWith('.yml')) {
|
|
const yamlReplacementRegExp = new RegExp(`${plan}: ['"]+${flag}['"]+`, 'g')
|
|
|
|
// Update versions in YAML files for feature-based versioning.
|
|
|
|
newContent = fileContent.replace(
|
|
yamlReplacementRegExp,
|
|
`${plan}: '${replacementYAMLValue}'`
|
|
)
|
|
|
|
// Update versions in Liquid conditionals.
|
|
|
|
newContent = newContent.replace(liquidReplacementRegExp, plan)
|
|
fs.writeFileSync(file, newContent, 'utf-8')
|
|
} else {
|
|
throw new Error(`Unknown file to toggle (${file}). Not a .yml or .md file.`)
|
|
}
|
|
})
|
|
|
|
let filesUpdatedForFlag = 0
|
|
|
|
if (allFlags[flag].length) {
|
|
console.log(`Toggled ${flag}. Committing changes...`)
|
|
|
|
allFlags[flag].forEach((fileToAdd) => {
|
|
execSync(`git add ${fileToAdd}`)
|
|
filesUpdatedForFlag++
|
|
})
|
|
|
|
if (filesUpdatedForFlag === allFlags[flag].length) {
|
|
let commitCommand = `git commit -m 'Toggle ${plan}-${flag} flag'`
|
|
|
|
if (flag.match(/^issue-[0-9]+$/)) {
|
|
commitCommand = `${commitCommand} -m 'For ${flag.replace(
|
|
'issue-',
|
|
'github/docs-content#'
|
|
)}'`
|
|
}
|
|
|
|
execSync(commitCommand)
|
|
commitCount++
|
|
}
|
|
}
|
|
|
|
// Check out any file that had syntax adjusted, but didn't contain one
|
|
// or more feature flags to toggle.
|
|
|
|
execSync(`git checkout --quiet ${contentDir} ${reusablesDir} ${dataDir}`)
|
|
})
|
|
|
|
if (commitCount > 0) {
|
|
console.log('Done!')
|
|
console.log(' - Review commits:')
|
|
console.log(` git log -n ${commitCount}`)
|
|
console.log(' - Review changes in diffs:')
|
|
console.log(` git show -n ${commitCount}`)
|
|
console.log(' - Undo changes:')
|
|
console.log(` git reset HEAD~${commitCount} && git checkout content data`)
|
|
console.log(' - Push changes:')
|
|
console.log(' git push')
|
|
}
|
|
}
|