semantic-release/lib/get-git-auth-url.js
Matt Travi ab45ab1f8d
chore(lint): disabled rules that dont apply to this project (#2408)
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
2022-04-02 15:33:08 -07:00

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;
};