xen-orchestra/scripts/gen-deps-list.js
Julien Fontanet f0da94081b feat(gen-deps-list): detect duplicate packages
Prevents a bug where a second entry would override the previous one and possibly
decrease the release type (e.g. `major + patch → patch`).
2023-12-07 17:15:09 +01:00

235 lines
6.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict'
const fs = require('fs').promises
const joinPath = require('path').join
const semver = require('semver')
const { getPackages } = require('./utils')
const escapeRegExp = require('lodash/escapeRegExp')
const invert = require('lodash/invert')
const keyBy = require('lodash/keyBy')
const { debug } = require('../@xen-orchestra/log').createLogger('gen-deps-list')
const computeDepOrder = require('./_computeDepOrder.js')
const changelogConfig = {
path: joinPath(__dirname, '../CHANGELOG.unreleased.md'),
startTag: '<!--packages-start-->',
endTag: '<!--packages-end-->',
}
const RELEASE_WEIGHT = { PATCH: 1, MINOR: 2, MAJOR: 3 }
const RELEASE_TYPE = invert(RELEASE_WEIGHT)
/** @type {Map<string, int>} A mapping of package names to their release weight */
const packagesToRelease = new Map()
let allPackages
async function main(args, scriptName) {
const toRelease = { __proto__: null }
const testMode = args[0] === '--test'
if (testMode) {
debug('reading packages from CLI')
args.shift()
for (const arg of args) {
const matches = /^(.+)@(patch|minor|major)$/.exec(arg)
if (matches === null) {
throw new Error('invalid arg: ' + arg)
}
toRelease[matches[1]] = matches[2]
}
} else {
if (args.length !== 0) {
process.stderr.write(`Usage:
${scriptName}
Read the list of packages with changes from \`CHANGELOG.unreleased.md\` and compute the list of packages to be released.
${scriptName} --test <name>@<release type>...
Compute the list of packages to be released from the list of changed packages from the command line.
Does not do any side effects.
`)
process.exitCode = 1
return
}
await readPackagesFromChangelog(toRelease)
}
allPackages = keyBy(await getPackages(true), 'name')
const releaseOrder = computeDepOrder(allPackages)
Object.entries(toRelease).forEach(([packageName, releaseType]) => {
const rootPackage = allPackages[packageName]
if (!rootPackage) {
throw new Error(`Package "${packageName}" does not exist`)
}
const rootReleaseWeight = releaseTypeToWeight(releaseType)
registerPackageToRelease(packageName, rootReleaseWeight)
handlePackageDependencies(rootPackage.name, getNextVersion(rootPackage.package.version, rootReleaseWeight))
})
const commandsToExecute = ['', 'Commands to execute:', '']
const releasedPackages = ['', '### Released packages', '']
releaseOrder.forEach(name => {
if (packagesToRelease.has(name)) {
const releaseWeight = packagesToRelease.get(name)
const {
package: { version },
} = allPackages[name]
commandsToExecute.push(`./scripts/bump-pkg ${name} ${RELEASE_TYPE[releaseWeight].toLocaleLowerCase()}`)
releasedPackages.push(`- ${name} ${getNextVersion(version, releaseWeight)}`)
}
})
console.log(commandsToExecute.join('\n'))
console.log(releasedPackages.join('\n'))
}
async function readPackagesFromChangelog(toRelease) {
debug('reading packages from CHANGELOG.unreleased.md')
const content = await fs.readFile(changelogConfig.path)
const changelogRegex = new RegExp(
`${escapeRegExp(changelogConfig.startTag)}(.*)${escapeRegExp(changelogConfig.endTag)}`,
's'
)
const block = changelogRegex.exec(content)?.[1].trim()
if (block === undefined) {
throw new Error(`Could not find changelog block in ${changelogConfig.path}`)
}
block.split('\n').forEach(rawLine => {
const line = rawLine.trim()
if (!line) {
return
}
const match = line.match(/^-\s*(?<name>\S+)\s+(?<releaseType>patch|minor|major)$/)
if (!match) {
throw new Error(`Invalid line: "${rawLine}"`)
}
const { name, releaseType } = match.groups
if (name in toRelease) {
throw new Error('duplicate package to release in CHANGELOG.unreleased.md: ' + name)
}
toRelease[name] = releaseType
})
}
/**
* Recursively add dependencies to the dependency tree
*
* @param {string} packageName The name of the package to handle
* @param {string} packageNextVersion The next version of the package to handle
*/
function handlePackageDependencies(packageName, packageNextVersion) {
Object.values(allPackages).forEach(
({ package: { name, version, dependencies, optionalDependencies, peerDependencies } }) => {
let releaseWeight
if (
shouldPackageBeReleased(name, { ...dependencies, ...optionalDependencies }, packageName, packageNextVersion)
) {
releaseWeight = RELEASE_WEIGHT.PATCH
debug('new compatible release due to dependency update', {
package: name,
dependency: packageName,
version: getNextVersion(version, releaseWeight),
})
} else if (shouldPackageBeReleased(name, peerDependencies || {}, packageName, packageNextVersion)) {
releaseWeight = versionToReleaseWeight(version)
debug('new breaking release due to peer dependency update', {
package: name,
peerDependency: packageName,
version: getNextVersion(version, releaseWeight),
})
}
if (releaseWeight !== undefined) {
registerPackageToRelease(name, releaseWeight)
handlePackageDependencies(name, getNextVersion(version, releaseWeight))
}
}
)
}
function releaseTypeToWeight(type) {
return RELEASE_WEIGHT[type.toLocaleUpperCase()]
}
/**
* @param {string} name The package name to check
* @param {object} dependencies The package dependencies name/constraint map
* @param {string} depName The name of the current dependency
* @param {string} depVersion The version to check the dependency constraint against
* @returns {boolean}
*/
function shouldPackageBeReleased(name, dependencies, depName, depVersion) {
if (!Object.prototype.hasOwnProperty.call(dependencies, depName)) {
return false
}
if (['xo-web', 'xo-server', '@xen-orchestra/proxy'].includes(name)) {
debug('forced release due to dependency update', {
package: name,
dependency: depName,
})
return true
}
return !semver.satisfies(depVersion, dependencies[depName])
}
/**
* @param {string} packageName
* @param {int} releaseWeight
*/
function registerPackageToRelease(packageName, releaseWeight) {
const currentWeight = packagesToRelease.get(packageName) || 0
packagesToRelease.set(packageName, Math.max(currentWeight, releaseWeight))
}
/**
* @param {string} version
* @returns {int}
*/
function versionToReleaseWeight(version) {
return semver.major(version) > 0
? RELEASE_WEIGHT.MAJOR
: semver.minor(version) > 0
? RELEASE_WEIGHT.MINOR
: RELEASE_WEIGHT.PATCH
}
/**
* @param {string} version The version to increment
* @param {int} releaseWeight The release weight to apply
* @returns {string} The incremented version
*/
function getNextVersion(version, releaseWeight) {
return semver.inc(version, RELEASE_TYPE[releaseWeight].toLocaleLowerCase())
}
main(process.argv.slice(2), process.argv[1]).catch(error => {
console.error(error)
process.exit(1)
})