Skip to content

Commit

Permalink
feat: semantic release update deps (#24)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
amcgee authored Mar 25, 2019
1 parent 7f11b69 commit d2c155e
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 45 deletions.
16 changes: 16 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
33 changes: 17 additions & 16 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -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
- [email protected]
- GIT_COMMITTER_NAME=@dhis2-bot
- [email protected]
- 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
- [email protected]
- GIT_COMMITTER_NAME=@dhis2-bot
- [email protected]
- 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=
92 changes: 63 additions & 29 deletions packages/d2-app/src/commands/scripts/release.js
Original file line number Diff line number Diff line change
@@ -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),
},
]
})
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 || '<unnamed>'
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 }

0 comments on commit d2c155e

Please sign in to comment.