the performance and readability of reduce is not a concern in this project since maintainers are familiar with the idiom and are iterating over small lists. the filter rule is disabled selectively since the filter being identified is not Array.filter closes #2403
123 lines
4.8 KiB
JavaScript
123 lines
4.8 KiB
JavaScript
const {parse, format} = require('url'); // eslint-disable-line node/no-deprecated-api
|
|
const {isNil} = require('lodash');
|
|
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
|
|
* - The `repositoryUrl` converted to `https` or `http` with Basic Authentication
|
|
*
|
|
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
|
|
*
|
|
* @param {Object} context semantic-release context.
|
|
*
|
|
* @return {String} The formatted Git repository URL.
|
|
*/
|
|
module.exports = async (context) => {
|
|
const {cwd, env, branch} = context;
|
|
const GIT_TOKENS = {
|
|
GIT_CREDENTIALS: undefined,
|
|
GH_TOKEN: undefined,
|
|
// GitHub Actions require the "x-access-token:" prefix for git access
|
|
// https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation
|
|
GITHUB_TOKEN: isNil(env.GITHUB_ACTION) ? undefined : 'x-access-token:',
|
|
GL_TOKEN: 'gitlab-ci-token:',
|
|
GITLAB_TOKEN: 'gitlab-ci-token:',
|
|
BB_TOKEN: 'x-token-auth:',
|
|
BITBUCKET_TOKEN: 'x-token-auth:',
|
|
BB_TOKEN_BASIC_AUTH: '',
|
|
BITBUCKET_TOKEN_BASIC_AUTH: '',
|
|
};
|
|
|
|
let {repositoryUrl} = context.options;
|
|
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
|
|
const {protocol, ...parsed} = parse(repositoryUrl);
|
|
|
|
if (info && info.getDefaultRepresentation() === 'shortcut') {
|
|
// Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
|
|
repositoryUrl = info.https();
|
|
} else if (protocol && protocol.includes('http')) {
|
|
// Replace `git+https` and `git+http` with `https` or `http`
|
|
repositoryUrl = format({...parsed, protocol: protocol.includes('https') ? 'https' : 'http', href: null});
|
|
}
|
|
|
|
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
|
|
try {
|
|
debug('Verifying ssh auth by attempting to push to %s', 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]));
|
|
|
|
// 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 (envVars.length > 1) {
|
|
debug(`Found ${envVars.length} credentials in environment, trying all of them`);
|
|
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
|
|
return repositoryUrl;
|
|
};
|