1
0
mirror of synced 2025-12-19 18:10:59 -05:00
Files
docs/script/toggle-ghae-feature-flags.js
Kevin Heis 05fe4c60cb Upgrade commander (#28613)
* Upgrade commander

* Change import
2022-06-17 16:43:02 +00:00

274 lines
8.5 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-next or 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} -n\n` +
` ${scriptName} -f 'issue-1234, issue-5678'`
)
.option('-n, --toggle-next', "toggle 'next' flags for M2 release")
.option('-s, --show-flags', 'show list of existing flags')
.option("-f, --toggle-flags '<flag-1>[,flag-2,...]'", 'toggle comma-separated list of flags')
.parse(process.argv)
const options = program.opts()
let optionsCount = 0
options.toggleNext && optionsCount++
options.showFlags && optionsCount++
options.toggleFlags && optionsCount++
// Show help with no options; error when more than one.
if (optionsCount === 0) {
program.help()
} else if (optionsCount > 1) {
console.log(`Error: you specified ${optionsCount} options (accepts 1)`)
process.exit(1)
}
// Store flags that user wants to toggle.
let flagsToToggle = []
let flagCount = 0
if (options.toggleNext) {
flagsToToggle = ['next']
} else if (options.toggleFlags) {
flagsToToggle = options.toggleFlags.split(',').map((item) => item.trim())
flagCount = flagsToToggle.length
}
if (options.toggleNext || 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)
}
}
// 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 === 'next') {
console.log(
`\n🚩 \x1b[7m ${plan}-${flag} \x1b[0m \x1b[1m\x1b[34m\x1b[4mhttps://github.com/github/docs-content/issues/3950\x1b[0m`
)
} else 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 or --toggle-next
} else if (options.toggleFlags || options.toggleNext) {
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, plan)
if (data.versions && data.versions[plan] === flag) {
// Update versions in content files with YAML front matter.
data.versions[plan] = '*'
}
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}: '*'`)
// 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')
}
}