Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bf377194e | ||
|
|
77a75f072b | ||
|
|
d74ffef788 | ||
|
|
3abcbaf256 |
+1
-1
@@ -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. 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.
|
||||
Running the integration test requires you to install [Docker](https://docs.docker.com/engine/installation) on your machine.
|
||||
|
||||
All the [semantic-release](https://github.com/semantic-release) repositories use [AVA](https://github.com/avajs/ava) for writing and running tests.
|
||||
|
||||
|
||||
@@ -105,3 +105,6 @@
|
||||
- `verifyConditions`: Locate and validate a `.gemspec` file, locate and validate a `lib/**/version.rb` file, verify the presence of the `GEM_HOST_API_KEY` environment variable, and create a credentials file with the API key.
|
||||
- `prepare`: Update the version in the `lib/**/version.rb` version file and [build](https://guides.rubygems.org/command-reference/#gem-build) the gem.
|
||||
- `publish`: [Push the Ruby gem](https://guides.rubygems.org/command-reference/#gem-push) to the gem server.
|
||||
- [semantic-release-npm-deprecate-old-versions](https://github.com/ghusse/semantic-release-npm-deprecate-old-versions)
|
||||
- `verifyConditions`: Validates configuration.
|
||||
- `publish`: Deprecates old versions, based on the declaration of supported versions in the config.
|
||||
|
||||
@@ -29,10 +29,6 @@ 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}\`.
|
||||
|
||||
+4
-19
@@ -1,15 +1,12 @@
|
||||
const {readFile} = require('fs').promises;
|
||||
const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash');
|
||||
const findPkgUp = require('pkg-up');
|
||||
const readPkgUp = require('read-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 = [
|
||||
@@ -77,7 +74,7 @@ module.exports = async (context, cliOptions) => {
|
||||
{name: 'beta', prerelease: true},
|
||||
{name: 'alpha', prerelease: true},
|
||||
],
|
||||
repositoryUrl: (await pkgRepoUrl({cwd})) || (await repoUrl({cwd, env})),
|
||||
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
|
||||
tagFormat: `v\${version}`,
|
||||
plugins: [
|
||||
'@semantic-release/commit-analyzer',
|
||||
@@ -96,18 +93,6 @@ module.exports = async (context, cliOptions) => {
|
||||
};
|
||||
|
||||
async function pkgRepoUrl(options) {
|
||||
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;
|
||||
const {packageJson} = (await readPkgUp(options)) || {};
|
||||
return packageJson && (isPlainObject(packageJson.repository) ? packageJson.repository.url : packageJson.repository);
|
||||
}
|
||||
|
||||
+66
-16
@@ -4,6 +4,48 @@ const hostedGitInfo = require('hosted-git-info');
|
||||
const {verifyAuth} = require('./git');
|
||||
const debug = require('debug')('semantic-release:get-git-auth-url');
|
||||
|
||||
/**
|
||||
* Machinery to format a repository URL with the given credentials
|
||||
*
|
||||
* @param {String} protocol URL protocol (which should not be present in repositoryUrl)
|
||||
* @param {String} repositoryUrl User-given repository URL
|
||||
* @param {String} gitCredentials The basic auth part of the URL
|
||||
*
|
||||
* @return {String} The formatted Git repository URL.
|
||||
*/
|
||||
function formatAuthUrl(protocol, repositoryUrl, gitCredentials) {
|
||||
const [match, auth, host, basePort, path] =
|
||||
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
|
||||
const {port, hostname, ...parsed} = parse(
|
||||
match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
|
||||
);
|
||||
|
||||
return format({
|
||||
...parsed,
|
||||
auth: gitCredentials,
|
||||
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
|
||||
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify authUrl by calling git.verifyAuth, but don't throw on failure
|
||||
*
|
||||
* @param {Object} context semantic-release context.
|
||||
* @param {String} authUrl Repository URL to verify
|
||||
*
|
||||
* @return {String} The authUrl as is if the connection was successfull, null otherwise
|
||||
*/
|
||||
async function ensureValidAuthUrl({cwd, env, branch}, authUrl) {
|
||||
try {
|
||||
await verifyAuth(authUrl, branch.name, {cwd, env});
|
||||
return authUrl;
|
||||
} catch (error) {
|
||||
debug(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the the git repository URL to use to push, either:
|
||||
* - The `repositoryUrl` as is if allowed to push
|
||||
@@ -15,7 +57,8 @@ const debug = require('debug')('semantic-release:get-git-auth-url');
|
||||
*
|
||||
* @return {String} The formatted Git repository URL.
|
||||
*/
|
||||
module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
|
||||
module.exports = async (context) => {
|
||||
const {cwd, env, branch} = context;
|
||||
const GIT_TOKENS = {
|
||||
GIT_CREDENTIALS: undefined,
|
||||
GH_TOKEN: undefined,
|
||||
@@ -30,6 +73,7 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
|
||||
BITBUCKET_TOKEN_BASIC_AUTH: '',
|
||||
};
|
||||
|
||||
let {repositoryUrl} = context.options;
|
||||
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
|
||||
const {protocol, ...parsed} = parse(repositoryUrl);
|
||||
|
||||
@@ -47,24 +91,30 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
|
||||
await verifyAuth(repositoryUrl, branch.name, {cwd, env});
|
||||
} catch (_) {
|
||||
debug('SSH key auth failed, falling back to https.');
|
||||
const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar]));
|
||||
|
||||
const envVar = Object.keys(GIT_TOKENS).find((envVar) => !isNil(env[envVar]));
|
||||
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
|
||||
// Skip verification if there is no ambiguity on which env var to use for authentication
|
||||
if (envVars.length === 1) {
|
||||
const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`;
|
||||
return formatAuthUrl(protocol, repositoryUrl, gitCredentials);
|
||||
}
|
||||
|
||||
if (gitCredentials) {
|
||||
// If credentials are set via environment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
|
||||
const [match, auth, host, path] =
|
||||
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<path>.*)$/.exec(repositoryUrl) || [];
|
||||
const {port, hostname, ...parsed} = parse(
|
||||
match ? `ssh://${auth ? `${auth}@` : ''}${host}/${path}` : repositoryUrl
|
||||
);
|
||||
if (envVars.length > 1) {
|
||||
debug(`Found ${envVars.length} credentials in environment, trying all of them`);
|
||||
|
||||
return format({
|
||||
...parsed,
|
||||
auth: gitCredentials,
|
||||
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
|
||||
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
|
||||
});
|
||||
const candidateRepositoryUrls = [];
|
||||
for (const envVar of envVars) {
|
||||
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`;
|
||||
const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials);
|
||||
candidateRepositoryUrls.push(ensureValidAuthUrl(context, authUrl));
|
||||
}
|
||||
|
||||
const validRepositoryUrls = await Promise.all(candidateRepositoryUrls);
|
||||
const chosenAuthUrlIndex = validRepositoryUrls.findIndex((url) => url !== null);
|
||||
if (chosenAuthUrlIndex > -1) {
|
||||
debug(`Using "${envVars[chosenAuthUrlIndex]}" to authenticate`);
|
||||
return validRepositoryUrls[chosenAuthUrlIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -31,7 +31,6 @@
|
||||
"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",
|
||||
@@ -43,7 +42,7 @@
|
||||
"micromatch": "^4.0.2",
|
||||
"p-each-series": "^2.1.0",
|
||||
"p-reduce": "^2.0.0",
|
||||
"pkg-up": "^3.1.0",
|
||||
"read-pkg-up": "^7.0.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"semver-diff": "^3.1.1",
|
||||
@@ -129,8 +128,7 @@
|
||||
"prettier": true,
|
||||
"space": true,
|
||||
"rules": {
|
||||
"unicorn/string-content": "off",
|
||||
"node/no-unsupported-features/node-builtins": "off"
|
||||
"unicorn/string-content": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,21 +516,3 @@ 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.');
|
||||
});
|
||||
|
||||
@@ -133,6 +133,32 @@ test('Return the "https" formatted URL if "gitCredentials" is defined and reposi
|
||||
);
|
||||
});
|
||||
|
||||
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user and with a custom port', async (t) => {
|
||||
const {cwd} = await gitRepo();
|
||||
|
||||
t.is(
|
||||
await getAuthUrl({
|
||||
cwd,
|
||||
env: {...env, GIT_CREDENTIALS: 'user:pass'},
|
||||
options: {branch: 'master', repositoryUrl: 'host.null:6666:owner/repo.git'},
|
||||
}),
|
||||
'https://user:pass@host.null:6666/owner/repo.git'
|
||||
);
|
||||
});
|
||||
|
||||
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL without user and with a custom port followed by a slash', async (t) => {
|
||||
const {cwd} = await gitRepo();
|
||||
|
||||
t.is(
|
||||
await getAuthUrl({
|
||||
cwd,
|
||||
env: {...env, GIT_CREDENTIALS: 'user:pass'},
|
||||
options: {branch: 'master', repositoryUrl: 'host.null:6666:/owner/repo.git'},
|
||||
}),
|
||||
'https://user:pass@host.null:6666/owner/repo.git'
|
||||
);
|
||||
});
|
||||
|
||||
test('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', async (t) => {
|
||||
const {cwd} = await gitRepo();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const {writeJson, readJson} = require('fs-extra');
|
||||
const execa = require('execa');
|
||||
const {WritableStreamBuffer} = require('stream-buffers');
|
||||
const delay = require('delay');
|
||||
const getAuthUrl = require('../lib/get-git-auth-url');
|
||||
const {SECRET_REPLACEMENT} = require('../lib/definitions/constants');
|
||||
const {
|
||||
gitHead,
|
||||
@@ -656,3 +657,43 @@ test('Hide sensitive environment variable values from the logs', async (t) => {
|
||||
t.regex(stderr, new RegExp(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`));
|
||||
t.regex(stderr, new RegExp(`Throw error: Exposing ${escapeRegExp(SECRET_REPLACEMENT)}`));
|
||||
});
|
||||
|
||||
test('Use the valid git credentials when multiple are provided', async (t) => {
|
||||
const {cwd, authUrl} = await gitbox.createRepo('test-auth');
|
||||
|
||||
t.is(
|
||||
await getAuthUrl({
|
||||
cwd,
|
||||
env: {
|
||||
GITHUB_TOKEN: 'dummy',
|
||||
GITLAB_TOKEN: 'trash',
|
||||
BB_TOKEN_BASIC_AUTH: gitbox.gitCredential,
|
||||
GIT_ASKPASS: 'echo',
|
||||
GIT_TERMINAL_PROMPT: 0,
|
||||
},
|
||||
branch: {name: 'master'},
|
||||
options: {repositoryUrl: 'http://toto@localhost:2080/git/test-auth.git'},
|
||||
}),
|
||||
authUrl
|
||||
);
|
||||
});
|
||||
|
||||
test('Use the repository URL as is if none of the given git credentials are valid', async (t) => {
|
||||
const {cwd} = await gitbox.createRepo('test-invalid-auth');
|
||||
const dummyUrl = 'http://toto@localhost:2080/git/test-auth.git';
|
||||
|
||||
t.is(
|
||||
await getAuthUrl({
|
||||
cwd,
|
||||
env: {
|
||||
GITHUB_TOKEN: 'dummy',
|
||||
GITLAB_TOKEN: 'trash',
|
||||
GIT_ASKPASS: 'echo',
|
||||
GIT_TERMINAL_PROMPT: 0,
|
||||
},
|
||||
branch: {name: 'master'},
|
||||
options: {repositoryUrl: dummyUrl},
|
||||
}),
|
||||
dummyUrl
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user