From 00bebd405458749af43a3ce7fc86e237edb2d957 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 30 May 2024 11:18:17 +0100 Subject: [PATCH] fix!: revert to semantic-release-monorepo (#1536) `@anolilab/multi-semantic-release` does not cause semantic release to check the commits of each package before working out if anything has changed so creates unecessary comments on long-closed github issues. `semantic-release-monorepo` did not do this and has been updated to be ESM-compatible recently so switch back to that. BREAKING CHANGE: if using `aegir release` in the scripts of a monorepo package, config must be updated: 1. Change `"release": "aegir release"` in the monorepo root to `"release": "aegir run release"` 2. Add `"release": "aegir release"` to each monorepo package 3. Add semantic release config to each monorepo package --- .aegir.js | 2 +- package.json | 4 +- src/align-versions.js | 128 ++++++++++++++++++++++++++++++++++++ src/cmds/align-versions.js | 46 +++++++++++++ src/cmds/release.js | 22 +------ src/index.js | 2 + src/release.js | 131 ++----------------------------------- 7 files changed, 187 insertions(+), 148 deletions(-) create mode 100644 src/align-versions.js create mode 100644 src/cmds/align-versions.js diff --git a/.aegir.js b/.aegir.js index 786aad205..1a6e2c303 100644 --- a/.aegir.js +++ b/.aegir.js @@ -17,7 +17,7 @@ export default { 'playwright-test', 'react-native-test-runner', 'semantic-release', - '@anolilab/multi-semantic-release', + 'semantic-release-monorepo', 'source-map-support', 'typedoc-plugin-mdn-links', 'typedoc-plugin-missing-exports', diff --git a/package.json b/package.json index 458eb4834..e149414b0 100644 --- a/package.json +++ b/package.json @@ -222,7 +222,6 @@ "release": "node src/index.js release --no-bundle" }, "dependencies": { - "@anolilab/multi-semantic-release": "^1.0.3", "@electron/get": "^3.0.0", "@polka/send-type": "^0.5.2", "@semantic-release/changelog": "^6.0.1", @@ -263,6 +262,7 @@ "eslint-plugin-promise": "^6.1.1", "execa": "^8.0.1", "extract-zip": "^2.0.1", + "fast-glob": "^3.3.2", "fs-extra": "^11.1.0", "gh-pages": "^6.0.0", "globby": "^14.0.0", @@ -283,7 +283,6 @@ "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.1", - "fast-glob": "^3.3.2", "mocha": "^10.0.0", "npm-package-json-lint": "^7.0.0", "nyc": "^15.1.0", @@ -300,6 +299,7 @@ "read-pkg-up": "^11.0.0", "rimraf": "^5.0.0", "semantic-release": "^23.0.0", + "semantic-release-monorepo": "^8.0.2", "semver": "^7.3.8", "source-map-support": "^0.5.20", "strip-bom": "^5.0.0", diff --git a/src/align-versions.js b/src/align-versions.js new file mode 100644 index 000000000..24ffb8be4 --- /dev/null +++ b/src/align-versions.js @@ -0,0 +1,128 @@ +/* eslint-disable no-console */ + +import path from 'path' +import { execa } from 'execa' +import fs from 'fs-extra' +import Listr from 'listr' +import { calculateSiblingVersion } from './check-project/utils.js' +import { isMonorepoRoot, getSubprojectDirectories, pkg } from './utils.js' + +/** + * @typedef {import("./types.js").GlobalOptions} GlobalOptions + * @typedef {import("./types.js").ReleaseOptions} ReleaseOptions + * @typedef {import("listr").ListrTaskWrapper} Task + */ + +const tasks = new Listr([ + { + title: 'align sibling dependency versions', + enabled: () => isMonorepoRoot(), + /** + * @param {GlobalOptions & ReleaseOptions} ctx + */ + task: async (ctx) => { + const rootDir = process.cwd() + const workspaces = pkg.workspaces + + if (!workspaces || !Array.isArray(workspaces)) { + throw new Error('No monorepo workspaces found') + } + + const { + siblingVersions, + packageDirs + } = await calculateSiblingVersions(rootDir, workspaces) + + // check these dependency types for monorepo siblings + const dependencyTypes = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies' + ] + + // align the versions of siblings in each package + for (const packageDir of packageDirs) { + const manifestPath = path.join(packageDir, 'package.json') + const manifest = fs.readJSONSync(path.join(packageDir, 'package.json')) + + for (const type of dependencyTypes) { + for (const [dep, version] of Object.entries(siblingVersions)) { + if (manifest[type] != null && manifest[type][dep] != null && manifest[type][dep] !== version) { + console.info('Update', type, dep, manifest[type][dep], '->', version) // eslint-disable-line no-console + manifest[type][dep] = version + } + } + } + + fs.writeJSONSync(manifestPath, manifest, { + spaces: 2 + }) + } + + // all done, commit changes and push to remote + const status = await execa('git', ['status', '--porcelain'], { + cwd: rootDir + }) + + if (status.stdout === '') { + // no changes, nothing to do + return + } + + if (!process.env.CI) { + console.info('CI env var is not set, not pushing to git') // eslint-disable-line no-console + return + } + + // When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. + // Borrowed from `semantic-release` + process.env.GIT_AUTHOR_NAME = ctx.siblingDepUpdateName + process.env.GIT_AUTHOR_EMAIL = ctx.siblingDepUpdateEmail + process.env.GIT_COMMITTER_NAME = ctx.siblingDepUpdateName + process.env.GIT_COMMITTER_EMAIL = ctx.siblingDepUpdateEmail + process.env.GIT_ASKPASS = 'echo' + process.env.GIT_TERMINAL_PROMPT = '0' + + console.info(`Commit with message "${ctx.siblingDepUpdateMessage}"`) // eslint-disable-line no-console + + await execa('git', ['add', '-A'], { + cwd: rootDir + }) + await execa('git', ['commit', '-m', ctx.siblingDepUpdateMessage], { + cwd: rootDir + }) + console.info('Push to remote') // eslint-disable-line no-console + await execa('git', ['push'], { + cwd: rootDir + }) + } + } +], { renderer: 'verbose' }) + +/** + * @param {string} rootDir + * @param {string[]} workspaces + */ +async function calculateSiblingVersions (rootDir, workspaces) { + const packageDirs = [] + + /** @type {Record} */ + const siblingVersions = {} + + for (const subProjectDir of await getSubprojectDirectories(rootDir, workspaces)) { + const pkg = JSON.parse(fs.readFileSync(path.join(subProjectDir, 'package.json'), { + encoding: 'utf-8' + })) + + siblingVersions[pkg.name] = calculateSiblingVersion(pkg.version) + packageDirs.push(subProjectDir) + } + + return { + packageDirs, + siblingVersions + } +} + +export default tasks diff --git a/src/cmds/align-versions.js b/src/cmds/align-versions.js new file mode 100644 index 000000000..ba00d0628 --- /dev/null +++ b/src/cmds/align-versions.js @@ -0,0 +1,46 @@ +import alignVersions from '../align-versions.js' +import { loadUserConfig } from '../config/user.js' + +/** + * @typedef {import("yargs").Argv} Argv + * @typedef {import("yargs").Arguments} Arguments + * @typedef {import("yargs").CommandModule} CommandModule + */ + +/** @type {CommandModule} */ +export default { + command: 'align-versions', + describe: 'Align monorepo sibling dependency versions', + /** + * @param {Argv} yargs + */ + builder: async (yargs) => { + const userConfig = await loadUserConfig() + + return yargs + .options({ + siblingDepUpdateMessage: { + alias: 'm', + type: 'string', + describe: 'The commit message to use when updating sibling dependencies', + default: userConfig.release.siblingDepUpdateMessage + }, + siblingDepUpdateName: { + type: 'string', + describe: 'The user name to use when updating sibling dependencies', + default: userConfig.release.siblingDepUpdateName + }, + siblingDepUpdateEmail: { + type: 'string', + describe: 'The email to use when updating sibling dependencies', + default: userConfig.release.siblingDepUpdateEmail + } + }) + }, + /** + * @param {any} argv + */ + async handler (argv) { + await alignVersions.run(argv) + } +} diff --git a/src/cmds/release.js b/src/cmds/release.js index c2d4c919a..f1753918f 100644 --- a/src/cmds/release.js +++ b/src/cmds/release.js @@ -1,4 +1,3 @@ -import { loadUserConfig } from '../config/user.js' import releaseCmd from '../release.js' /** @@ -19,28 +18,9 @@ export default { * @param {Argv} yargs */ builder: async (yargs) => { - const userConfig = await loadUserConfig() - return yargs .epilog(EPILOG) - .options({ - siblingDepUpdateMessage: { - alias: 'm', - type: 'string', - describe: 'The commit message to use when updating sibling dependencies', - default: userConfig.release.siblingDepUpdateMessage - }, - siblingDepUpdateName: { - type: 'string', - describe: 'The user name to use when updating sibling dependencies', - default: userConfig.release.siblingDepUpdateName - }, - siblingDepUpdateEmail: { - type: 'string', - describe: 'The email to use when updating sibling dependencies', - default: userConfig.release.siblingDepUpdateEmail - } - }) + .options({}) }, /** * @param {any} argv diff --git a/src/index.js b/src/index.js index c5c3db23e..344ef0ea9 100755 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import { readPackageUpSync } from 'read-pkg-up' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' +import alignVersionsCmd from './cmds/align-versions.js' import buildCmd from './cmds/build.js' import checkProjectCmd from './cmds/check-project.js' import checkCmd from './cmds/check.js' @@ -92,6 +93,7 @@ async function main () { res.command(testCmd) res.command(execCmd) res.command(runCmd) + res.command(alignVersionsCmd) try { await res.parse() diff --git a/src/release.js b/src/release.js index 10ee1a4f4..fcfc5c54b 100644 --- a/src/release.js +++ b/src/release.js @@ -1,11 +1,8 @@ /* eslint-disable no-console */ -import path from 'path' import { execa } from 'execa' -import fs from 'fs-extra' import Listr from 'listr' -import { calculateSiblingVersion } from './check-project/utils.js' -import { isMonorepoProject, isMonorepoRoot, hasDocs, getSubprojectDirectories } from './utils.js' +import { isMonorepoProject, hasDocs, pkg } from './utils.js' /** * @typedef {import("./types").GlobalOptions} GlobalOptions @@ -29,104 +26,15 @@ const tasks = new Listr([ * @param {GlobalOptions} ctx */ task: async (ctx) => { - const args = ctx['--'] ?? [] + let args = ctx['--'] ?? [] - if (isMonorepoRoot()) { - await execa('multi-semantic-release', [ - // eslint-disable-next-line no-template-curly-in-string - '--tag-format', '${name}-${version}', - '--deps.bump', 'satisfy', - '--deps.release', 'minor', - ...args - ], { - preferLocal: true, - stdio: 'inherit' - }) - } else { - await execa('semantic-release', args, { - preferLocal: true, - stdio: 'inherit' - }) + if (isMonorepoProject()) { + args = ['-e', 'semantic-release-monorepo', `--tag-format="${pkg.name}-\${version}"`, ...args] } - } - }, - { - title: 'align sibling dependency versions', - enabled: () => isMonorepoProject(), - /** - * @param {GlobalOptions & ReleaseOptions} ctx - */ - task: async (ctx) => { - const parentManifestPath = path.resolve(path.join(process.cwd(), '..', '..', 'package.json')) - const rootDir = path.dirname(parentManifestPath) - const parentManifest = fs.readJSONSync(parentManifestPath) - const workspaces = parentManifest.workspaces - - if (!workspaces || !Array.isArray(workspaces)) { - throw new Error('No monorepo workspaces found') - } - - const { - siblingVersions, - packageDirs - } = await calculateSiblingVersions(rootDir, workspaces) - - // check these dependency types for monorepo siblings - const dependencyTypes = [ - 'dependencies', - 'devDependencies', - 'peerDependencies', - 'optionalDependencies' - ] - - // align the versions of siblings in each package - for (const packageDir of packageDirs) { - const manifestPath = path.join(packageDir, 'package.json') - const manifest = fs.readJSONSync(path.join(packageDir, 'package.json')) - - for (const type of dependencyTypes) { - for (const [dep, version] of Object.entries(siblingVersions)) { - if (manifest[type] != null && manifest[type][dep] != null && manifest[type][dep] !== version) { - console.info('Update', type, dep, manifest[type][dep], '->', version) // eslint-disable-line no-console - manifest[type][dep] = version - } - } - } - - fs.writeJSONSync(manifestPath, manifest, { - spaces: 2 - }) - } - - // all done, commit changes and push to remote - const status = await execa('git', ['status', '--porcelain'], { - cwd: rootDir - }) - if (status.stdout === '') { - // no changes, nothing to do - return - } - - // When running on CI, set the commits author and commiter info and prevent the `git` CLI to prompt for username/password. - // Borrowed from `semantic-release` - process.env.GIT_AUTHOR_NAME = ctx.siblingDepUpdateName - process.env.GIT_AUTHOR_EMAIL = ctx.siblingDepUpdateEmail - process.env.GIT_COMMITTER_NAME = ctx.siblingDepUpdateName - process.env.GIT_COMMITTER_EMAIL = ctx.siblingDepUpdateEmail - process.env.GIT_ASKPASS = 'echo' - process.env.GIT_TERMINAL_PROMPT = '0' - - console.info(`Commit with message "${ctx.siblingDepUpdateMessage}"`) // eslint-disable-line no-console - await execa('git', ['add', '-A'], { - cwd: rootDir - }) - await execa('git', ['commit', '-m', ctx.siblingDepUpdateMessage], { - cwd: rootDir - }) - console.info('Push to remote') // eslint-disable-line no-console - await execa('git', ['push'], { - cwd: rootDir + await execa('semantic-release', args, { + preferLocal: true, + stdio: 'inherit' }) } }, @@ -141,29 +49,4 @@ const tasks = new Listr([ } ], { renderer: 'verbose' }) -/** - * @param {string} rootDir - * @param {string[]} workspaces - */ -async function calculateSiblingVersions (rootDir, workspaces) { - const packageDirs = [] - - /** @type {Record} */ - const siblingVersions = {} - - for (const subProjectDir of await getSubprojectDirectories(rootDir, workspaces)) { - const pkg = JSON.parse(fs.readFileSync(path.join(subProjectDir, 'package.json'), { - encoding: 'utf-8' - })) - - siblingVersions[pkg.name] = calculateSiblingVersion(pkg.version) - packageDirs.push(subProjectDir) - } - - return { - packageDirs, - siblingVersions - } -} - export default tasks