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