From d2c155e0eb037daccffc4398a73be0207b9e82f3 Mon Sep 17 00:00:00 2001 From: Austin McGee <947888+amcgee@users.noreply.github.com> Date: Mon, 25 Mar 2019 11:18:27 +0100 Subject: [PATCH] feat: semantic release update deps (#24) * chore: clean up plugin array * fix: add all package.json and yarn.lock files * feat: implement monorepo dependency updates as a semantic-release plugin * fix: update travis release command * chore: filter publishable packages before adding npm publisher * chore: cleanup release handler * chore: support exact version dependencies (unused) * chore: fix sleep-deprivation relics * chore: add simple smoke test to travis config --- .editorconfig | 16 ++++ .travis.yml | 33 +++---- .../d2-app/src/commands/scripts/release.js | 92 ++++++++++++------ .../scripts/support/getWorkspacePackages.js | 46 +++++++++ .../support/normalizeAndValidatePackages.js | 48 ++++++++++ .../support/semantic-release-update-deps.js | 93 +++++++++++++++++++ 6 files changed, 283 insertions(+), 45 deletions(-) create mode 100644 .editorconfig create mode 100644 packages/d2-app/src/commands/scripts/support/getWorkspacePackages.js create mode 100644 packages/d2-app/src/commands/scripts/support/normalizeAndValidatePackages.js create mode 100644 packages/d2-app/src/commands/scripts/support/semantic-release-update-deps.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..57becb87 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# For more information about the properties used in +# this file, please see the EditorConfig documentation: +# https://editorconfig.org/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.travis.yml b/.travis.yml index 8df0834c..89df9ed5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,22 @@ language: node_js node_js: -- 10 + - 10 script: -- echo "No build script, proceed..." + - echo "Smoke testing d2 executable" + - ./packages/d2/bin/d2 debug system deploy: -- provider: script - skip_cleanup: true - script: - - ./packages/d2-app/bin/d2-app scripts release --publish mono-npm - on: - tags: false - branch: master + - provider: script + skip_cleanup: true + script: + - ./packages/d2-app/bin/d2-app scripts release --publish npm + on: + tags: false + branch: master env: - global: - - GIT_AUTHOR_NAME=@dhis2-bot - - GIT_AUTHOR_EMAIL=ci@dhis2.org - - GIT_COMMITTER_NAME=@dhis2-bot - - GIT_COMMITTER_EMAIL=ci@dhis2.org - - secure: CxBl9xFgVQhl/xFj8G9WrWKGcIB5cmdwYsSohmes2xvJZJ/2wDSC0PjewSk3BWT9CxiZ6Hv36S/54TL3q6xRsMl2GKawLtVkuozFtJd42G8wqrXVylQvix7HjgiYpPPK68sYnrV0Q3/9DK4StXYFr4JBWcgZ0pKL9KaaY7BeKMFkahRiVqa4yhTIZm1V1o6CYyQ4CKy+ZipDp9Igy+VNjSY6waTifQ5EuvL8DnETdtbp/tngUHbdXoNT+H4RQvd1f9ChRncUYlk6jTiBdGd5aN/ag0O1GY5SPQJJ4K/dyiQdbNUIb345z2tJjwr+FaUTm/fU6j+riZIs2sJ9oFlGylGokPSZQgozCtJTiP5JIP3uHtu3K9Tw/md/a1uZ8dIoCIQQoR8rx/zwlWpAVKs+ZudrNQKtnGiJ58qr5EezPuXQrMQAh+cMMkYfVRpdFv/c4yq/7SU1nK6y93gJnqw6I01nn4wNK77impIhdpwDTGbsrUdIeMojDb+VOoo3llwkeV9gRKquLZpKW1b/TXU9IQ/Icw+bN8SAnY6vqKxbg3vdBGrs9amYBIJGXxKxBZjYa+MnQiP/ipRpm3EDLn8XXiVS+VZSTCbr1cuEmJQPllTOV9c2Ov0Ie0KcBpevzfQ2p2uH+zYMSzE7+hkoQht2o+bVrSfiHqgSup8pAnYVmX4= - - secure: OVdVE/sQMl0kLT4SvKFr6yVlGGC6DiQyMdUjZM4tjfh/pMNVLm8EmMccnoUSfnuDjp7lU87gbjauVP4qbMBl2j9ZBBisRAArZgf4jJ3W9H+kQGrQB8COxgWXp85mVO3qvCbbNTOCQXKPiR51kEHV4xxPcbQzLNz9endqi6zyfOGdes4kEJY/gWX+TAtYMstHVUVN097S0bLMW6xjeodlnulcEzw2LFAXSDtGj1xtjkE6XrqBIIqIR2eW/gKwiB+WGLscn9pZln1I1F2upGSE/KVBzvFk02aV5md/2PkJLtYYRxrI5LwqRvKf0FazUfsjG2BlkSV7pGySvBeLeTz2hTvOjUjXM+w5VjpL00Qor9q20p+qXe31sS7dGr8Pk6vsXMsDljvEgi32k1VuumaU2RfgPYMFtH+gYam7SIchg8A/XtWQnL5hOybc7ceVM09uRUPBtEWAy6Wn5bczt6nTsXOmfcpYH+PMnIoStG+A/636nkEDTy30k2mOa3PW+Yjh7a8jOk+yHClvoZANw3cqKjL83lW5cP/LdWe3LT5UjSUjk9hd1ndEWAUbOBEAG6MACJniF9YuwGe25JJMHArOS+pGIM3AuZsBC5lXSnALMjH2Bqfgjuhcn+djWXEvTJGr5kCwchgtqlARWUh0J5B1nwx48jvUnCyyniReKdR6n5k= + global: + - GIT_AUTHOR_NAME=@dhis2-bot + - GIT_AUTHOR_EMAIL=ci@dhis2.org + - GIT_COMMITTER_NAME=@dhis2-bot + - GIT_COMMITTER_EMAIL=ci@dhis2.org + - secure: CxBl9xFgVQhl/xFj8G9WrWKGcIB5cmdwYsSohmes2xvJZJ/2wDSC0PjewSk3BWT9CxiZ6Hv36S/54TL3q6xRsMl2GKawLtVkuozFtJd42G8wqrXVylQvix7HjgiYpPPK68sYnrV0Q3/9DK4StXYFr4JBWcgZ0pKL9KaaY7BeKMFkahRiVqa4yhTIZm1V1o6CYyQ4CKy+ZipDp9Igy+VNjSY6waTifQ5EuvL8DnETdtbp/tngUHbdXoNT+H4RQvd1f9ChRncUYlk6jTiBdGd5aN/ag0O1GY5SPQJJ4K/dyiQdbNUIb345z2tJjwr+FaUTm/fU6j+riZIs2sJ9oFlGylGokPSZQgozCtJTiP5JIP3uHtu3K9Tw/md/a1uZ8dIoCIQQoR8rx/zwlWpAVKs+ZudrNQKtnGiJ58qr5EezPuXQrMQAh+cMMkYfVRpdFv/c4yq/7SU1nK6y93gJnqw6I01nn4wNK77impIhdpwDTGbsrUdIeMojDb+VOoo3llwkeV9gRKquLZpKW1b/TXU9IQ/Icw+bN8SAnY6vqKxbg3vdBGrs9amYBIJGXxKxBZjYa+MnQiP/ipRpm3EDLn8XXiVS+VZSTCbr1cuEmJQPllTOV9c2Ov0Ie0KcBpevzfQ2p2uH+zYMSzE7+hkoQht2o+bVrSfiHqgSup8pAnYVmX4= + - secure: OVdVE/sQMl0kLT4SvKFr6yVlGGC6DiQyMdUjZM4tjfh/pMNVLm8EmMccnoUSfnuDjp7lU87gbjauVP4qbMBl2j9ZBBisRAArZgf4jJ3W9H+kQGrQB8COxgWXp85mVO3qvCbbNTOCQXKPiR51kEHV4xxPcbQzLNz9endqi6zyfOGdes4kEJY/gWX+TAtYMstHVUVN097S0bLMW6xjeodlnulcEzw2LFAXSDtGj1xtjkE6XrqBIIqIR2eW/gKwiB+WGLscn9pZln1I1F2upGSE/KVBzvFk02aV5md/2PkJLtYYRxrI5LwqRvKf0FazUfsjG2BlkSV7pGySvBeLeTz2hTvOjUjXM+w5VjpL00Qor9q20p+qXe31sS7dGr8Pk6vsXMsDljvEgi32k1VuumaU2RfgPYMFtH+gYam7SIchg8A/XtWQnL5hOybc7ceVM09uRUPBtEWAy6Wn5bczt6nTsXOmfcpYH+PMnIoStG+A/636nkEDTy30k2mOa3PW+Yjh7a8jOk+yHClvoZANw3cqKjL83lW5cP/LdWe3LT5UjSUjk9hd1ndEWAUbOBEAG6MACJniF9YuwGe25JJMHArOS+pGIM3AuZsBC5lXSnALMjH2Bqfgjuhcn+djWXEvTJGr5kCwchgtqlARWUh0J5B1nwx48jvUnCyyniReKdR6n5k= diff --git a/packages/d2-app/src/commands/scripts/release.js b/packages/d2-app/src/commands/scripts/release.js index 96158c14..d362baae 100644 --- a/packages/d2-app/src/commands/scripts/release.js +++ b/packages/d2-app/src/commands/scripts/release.js @@ -1,23 +1,26 @@ const { reporter } = require('@dhis2/cli-helpers-engine') - -const { readdirSync } = require('fs') -const { join } = require('path') - +const { existsSync } = require('fs') +const path = require('path') const semanticRelease = require('semantic-release') +const getWorkspacePackages = require('./support/getWorkspacePackages') + +const packageIsPublishable = pkgJsonPath => { + try { + const pkgJson = require(pkgJsonPath) + return !!pkgJson.name && !pkgJson.private + } catch (e) { + return false + } +} -function publisher(target = '') { +function publisher(target = '', packages) { switch (target.toLowerCase()) { case 'npm': { - return ['@semantic-release/npm'] - } - - case 'mono-npm': { - const packages = readdirSync('./packages') - return packages.map(p => { + return packages.filter(packageIsPublishable).map(pkgJsonPath => { return [ '@semantic-release/npm', { - pkgRoot: join('./packages', p), + pkgRoot: path.dirname(pkgJsonPath), }, ] }) @@ -29,38 +32,69 @@ function publisher(target = '') { } } -const handler = async ({ name, publish }) => { +const handler = async ({ publish }) => { // set up the plugins and filter out any undefined elements - const plugins = [ - '@semantic-release/commit-analyzer', - '@semantic-release/release-notes-generator', - [ - '@semantic-release/changelog', - { - changelogFile: 'CHANGELOG.md', - }, - ], + const rootPackageFile = path.join(process.cwd(), 'package.json') + const packages = [ + rootPackageFile, + ...(await getWorkspacePackages(rootPackageFile)), ] - const pub = publisher(publish) - pub.map(p => plugins.push(p)) + const updateDepsPlugin = + packages.length > 1 + ? [ + require('./support/semantic-release-update-deps'), + { + packages, + }, + ] + : undefined + + const changelogPlugin = [ + '@semantic-release/changelog', + { + changelogFile: 'CHANGELOG.md', + }, + ] - plugins.push([ + const gitPlugin = [ '@semantic-release/git', { - assets: ['CHANGELOG.md', 'package.json', 'yarn.lock'], + assets: [ + 'CHANGELOG.md', + packages.map(pkgJsonPath => + path.relative(process.cwd(), pkgJsonPath) + ), + packages + .map(pkgJsonPath => + path.join(path.dirname(pkgJsonPath), 'yarn.lock') + ) + .filter(existsSync) + .map(pkgJsonPath => + path.relative(process.cwd(), pkgJsonPath) + ), + ], message: 'chore(release): cut ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', }, - ]) + ] - plugins.push('@semantic-release/github') + // Order matters here! + const plugins = [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + updateDepsPlugin, + changelogPlugin, + ...publisher(publish, packages), + gitPlugin, + '@semantic-release/github', + ] const options = { branch: 'master', version: 'v${version}', - plugins: plugins.filter(n => n), + plugins: plugins.filter(n => !!n), } const config = { diff --git a/packages/d2-app/src/commands/scripts/support/getWorkspacePackages.js b/packages/d2-app/src/commands/scripts/support/getWorkspacePackages.js new file mode 100644 index 00000000..b4500493 --- /dev/null +++ b/packages/d2-app/src/commands/scripts/support/getWorkspacePackages.js @@ -0,0 +1,46 @@ +const glob = require('glob') +const path = require('path') + +// Simplified from https://github.com/yarnpkg/yarn/blob/bb9741af4d1fe00adb15e4a7596c7a3472d0bda3/src/config.js#L814 +const globPackageFilePattern = pattern => + glob.sync( + path.join(process.cwd(), pattern.replace(/\/?$/, '/package.json')), + { + ignore: pattern.replace(/\/?$/, '/node_modules/**/package.json'), + } + ) +const getWorkspacePackages = async packageFile => { + try { + const rootPackage = require(packageFile) + if (rootPackage.workspaces) { + let workspaces + if (Array.isArray(rootPackage.workspaces)) { + workspaces = rootPackage.workspaces + } else { + workspaces = rootPackage.workspaces.packages + if (!workspaces || !workspaces.isArray(workspaces)) { + reporter.debug( + '[release::getWorkspacePackage] Invalid workspaces key-value in root package.json' + ) + return [] + } + } + + return workspaces.reduce( + (packages, wsPattern) => [ + ...packages, + ...globPackageFilePattern(wsPattern), + ], + [] + ) + } + } catch (e) { + reporter.debug( + '[release::getWorkspacePackage] Failed to load root package.json', + e + ) + } + return [] +} + +module.exports = getWorkspacePackages diff --git a/packages/d2-app/src/commands/scripts/support/normalizeAndValidatePackages.js b/packages/d2-app/src/commands/scripts/support/normalizeAndValidatePackages.js new file mode 100644 index 00000000..526ac3cf --- /dev/null +++ b/packages/d2-app/src/commands/scripts/support/normalizeAndValidatePackages.js @@ -0,0 +1,48 @@ +const fs = require('fs') +const path = require('path') + +const normalizeAndValidatePackages = packages => { + const errors = [] + const validPackages = [] + + packages.forEach(packagePath => { + let pkgJsonPath + if (!fs.existsSync(packagePath)) { + errors.push(`Path ${packagePath} does not exist`) + } else if (fs.statSync(packagePath).isDirectory()) { + pkgJsonPath = path.join(packagePath, 'package.json') + } else if (!packagePath.endsWith('package.json')) { + errors.push( + `Path ${packagePath} is not a package.json file or directory` + ) + } else { + pkgJsonPath = packagePath + } + + if ( + pkgJsonPath && + fs.existsSync(pkgJsonPath) && + fs.statSync(pkgJsonPath).isFile() + ) { + try { + const pkgJson = require(pkgJsonPath) + + validPackages.push({ + path: pkgJsonPath, + json: pkgJson, + }) + } catch (e) { + errors.push({ + message: `Failed to load package.json at ${pkgJsonPath}`, + details: e, + }) + } + } else { + errors.push(`Package at ${packagePath} not found`) + } + }) + + return [validPackages, errors] +} + +module.exports = normalizeAndValidatePackages diff --git a/packages/d2-app/src/commands/scripts/support/semantic-release-update-deps.js b/packages/d2-app/src/commands/scripts/support/semantic-release-update-deps.js new file mode 100644 index 00000000..f6ae3497 --- /dev/null +++ b/packages/d2-app/src/commands/scripts/support/semantic-release-update-deps.js @@ -0,0 +1,93 @@ +const fs = require('fs') +const path = require('path') +const SemanticReleaseError = require('@semantic-release/error') +const AggregateError = require('aggregate-error') +const normalizeAndValidatePackages = require('./normalizeAndValidatePackages') + +const verifyConditions = (config = {}, context) => { + const { silent, packages } = config + const { logger } = context + if (!packages || !packages.length || packages.length < 2) { + throw new SemanticReleaseError( + 'Invalid packages option', + 'EINVALIDPACKAGES', + 'You must pass at least two package directories to semantic-release-update-deps' + ) + } + + const [validPackages, errors] = normalizeAndValidatePackages(packages) + + if (errors.length) { + throw new AggregateError(errors) + } + + validPackages.forEach(package => { + package.label = package.json.name || '' + if (!silent) { + logger.log(`Package ${package.label} found at ${package.path}`) + } + }) + + context.packages = validPackages +} + +const replaceDependencies = (pkg, listNames, packageNames, version) => { + const dependencies = [] + packageNames.forEach(packageName => { + listNames.forEach(listName => { + if (pkg[listName] && pkg[listName][packageName]) { + pkg[listName][packageName] = version + dependencies.push(`${packageName} (${listName})`) + } + }) + }) + return dependencies +} + +const prepare = (config, context) => { + if (!context.packages) { + verifyConditions({ ...config, silent: true }, context) + } + const { silent, exact } = config + const { nextRelease, logger, packages } = context + + const targetVersion = exact + ? nextRelease.version + : `^${nextRelease.version}` + + const names = packages.map(package => package.json.name).filter(n => n) + packages.forEach(package => { + const pkgJson = package.json + const relativePath = path.relative(context.cwd, package.path) + + pkgJson.version = nextRelease.version + if (!silent) { + logger.log( + `Updated version to ${nextRelease.version} for package ${ + package.label + } at ${relativePath}` + ) + } + + replaceDependencies( + pkgJson, + ['dependencies', 'devDependencies', 'peerDependencies'], + names, + targetVersion + ).forEach( + dep => + !silent && + logger.log( + `Upgraded dependency ${dep}@${targetVersion} for ${ + package.label + } at ${relativePath}` + ) + ) + fs.writeFileSync( + package.path, + JSON.stringify(pkgJson, undefined, config.tabSpaces || 2) + '\n' + ) + }) +} + +module.exports = { verifyConditions, prepare }