docs/script/toggle-ghae-feature-flags.js

235 строки
6.9 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://git.io/JCtUO\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 = {}
console.log(`Parsing all flags for ${plan} plan on ${currentBranch} branch...`)
allFiles.forEach((file) => {
const fileContent = fs.readFileSync(file, 'utf8')
const { data } = frontmatter(fileContent)
// Process YAML front matter and data files.
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 liquidShowRegExp = new RegExp(`\\s${plan}-([^\\s]+)`, 'g')
const deduplicatedMatches = [...new Set(fileContent.match(liquidShowRegExp))]
if (deduplicatedMatches.length > 0) {
const matches = deduplicatedMatches.map((match) => match.trim().replace(plan + '-', ''))
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) => {
allFiles.forEach((file) => {
const fileContent = fs.readFileSync(file, 'utf8')
const { data } = frontmatter(fileContent)
const liquidReplacementRegExp = new RegExp(`${plan}-${flag}`, 'g')
// Update versions in Liquid conditionals.
const newContent = fileContent.replace(liquidReplacementRegExp, plan)
if (data.versions && data.versions[plan] === flag) {
// Update versions in YAML files and content files with YAML front
// matter.
data.versions[plan] = '*'
}
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 }))
})
let filesUpdatedForFlag = 0
if (!(flag in allFlags)) {
console.log(`Warning: ${flag} does not exist in source`)
}
if (allFlags[flag]) {
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}`)
})
console.log('Done!')
if (commitCount > 0) {
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')
}
}