diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3c40c5851..1f466461cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -229,7 +229,7 @@ Before pushing your code changes make sure there are no linting errors with `npm ### Tests -Running the integration test requires you to install [Docker](https://docs.docker.com/engine/installation) on your machine. +Running the integration test requires you to install [Docker](https://docs.docker.com/engine/installation) on your machine. Note: the tests assume that running `git init` will create a `master` branch by default. If your local `git` is configured differently (see [`init.defaultBranch`](https://github.blog/2020-07-27-highlights-from-git-2-28/#introducing-init-defaultbranch)), change it temporarily when running the tests. All the [semantic-release](https://github.com/semantic-release) repositories use [AVA](https://github.com/avajs/ava) for writing and running tests. diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index a602983803..b125b778d8 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -29,6 +29,10 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura 'docs/usage/configuration.md' )}).`, }), + EDUPLICATEREPOSITORYKEY: ({packageJsonPath}) => ({ + message: 'Duplicate `"repository"` key in package.json.', + details: `Your package.json file at ${packageJsonPath} has more than one "repository" keys.`, + }), EGITNOPERMISSION: ({options: {repositoryUrl}, branch: {name}}) => ({ message: 'Cannot push to the Git repository.', details: `**semantic-release** cannot push the version tag to the branch \`${name}\` on the remote Git repository with URL \`${repositoryUrl}\`. diff --git a/lib/get-config.js b/lib/get-config.js index bd40cecc1d..e7bf683284 100644 --- a/lib/get-config.js +++ b/lib/get-config.js @@ -1,12 +1,15 @@ +const {readFile} = require('fs').promises; const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash'); -const readPkgUp = require('read-pkg-up'); +const findPkgUp = require('pkg-up'); const {cosmiconfig} = require('cosmiconfig'); const resolveFrom = require('resolve-from'); +const findDuplicatedPropertyKeys = require('find-duplicated-property-keys'); const debug = require('debug')('semantic-release:config'); const {repoUrl} = require('./git'); const PLUGINS_DEFINITIONS = require('./definitions/plugins'); const plugins = require('./plugins'); const {validatePlugin, parseConfig} = require('./plugins/utils'); +const getError = require('./get-error'); const CONFIG_NAME = 'release'; const CONFIG_FILES = [ @@ -74,7 +77,7 @@ module.exports = async (context, cliOptions) => { {name: 'beta', prerelease: true}, {name: 'alpha', prerelease: true}, ], - repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})), + repositoryUrl: (await pkgRepoUrl({cwd})) || (await repoUrl({cwd, env})), tagFormat: `v\${version}`, plugins: [ '@semantic-release/commit-analyzer', @@ -93,6 +96,18 @@ module.exports = async (context, cliOptions) => { }; async function pkgRepoUrl(options) { - const {packageJson} = (await readPkgUp(options)) || {}; - return packageJson && (isPlainObject(packageJson.repository) ? packageJson.repository.url : packageJson.repository); + const packageJsonPath = await findPkgUp(options); + if (!packageJsonPath) return; + + const packageJsonString = await readFile(packageJsonPath, 'utf-8'); + const result = findDuplicatedPropertyKeys(packageJsonString); + + if (result.length > 0) { + throw getError('EDUPLICATEREPOSITORYKEY', {packageJsonPath}); + } + + const {repository} = require(packageJsonPath); + if (!repository) return; + + return isPlainObject(repository) ? repository.url : repository; } diff --git a/package.json b/package.json index fdbf3b90f0..12146b8893 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "env-ci": "^5.0.0", "execa": "^4.0.0", "figures": "^3.0.0", + "find-duplicated-property-keys": "^1.2.2", "find-versions": "^3.0.0", "get-stream": "^5.0.0", "git-log-parser": "^1.2.0", @@ -42,7 +43,7 @@ "micromatch": "^4.0.2", "p-each-series": "^2.1.0", "p-reduce": "^2.0.0", - "read-pkg-up": "^7.0.0", + "pkg-up": "^3.1.0", "resolve-from": "^5.0.0", "semver": "^7.3.2", "semver-diff": "^3.1.1", @@ -128,7 +129,8 @@ "prettier": true, "space": true, "rules": { - "unicorn/string-content": "off" + "unicorn/string-content": "off", + "node/no-unsupported-features/node-builtins": "off" } } } diff --git a/test/get-config.test.js b/test/get-config.test.js index 4e5b456708..f44a90b9d0 100644 --- a/test/get-config.test.js +++ b/test/get-config.test.js @@ -516,3 +516,21 @@ test('Throw an Error if one of the shareable config cannot be found', async (t) code: 'MODULE_NOT_FOUND', }); }); + +test('Throw an Error if package.json has duplicate "repository" key', async (t) => { + // Create a git repository, set the current working directory at the root of the repo + const {cwd} = await gitRepo(); + + // Create package.json with duplicate "repository" key + await writeFile( + path.resolve(cwd, 'package.json'), + `{ + "repository": "https://github.com/octocat/repository", + "repository": "https://github.com/octocat/repository" + }` + ); + + const error = await t.throwsAsync(t.context.getConfig({cwd})); + t.is(error.code, 'EDUPLICATEREPOSITORYKEY'); + t.is(error.message, 'Duplicate `"repository"` key in package.json.'); +});