UNPKG

4.91 kBJavaScriptView Raw
1const {parse, format} = require('url'); // eslint-disable-line node/no-deprecated-api
2const {isNil} = require('lodash');
3const hostedGitInfo = require('hosted-git-info');
4const {verifyAuth} = require('./git');
5const debug = require('debug')('semantic-release:get-git-auth-url');
6
7/**
8 * Machinery to format a repository URL with the given credentials
9 *
10 * @param {String} protocol URL protocol (which should not be present in repositoryUrl)
11 * @param {String} repositoryUrl User-given repository URL
12 * @param {String} gitCredentials The basic auth part of the URL
13 *
14 * @return {String} The formatted Git repository URL.
15 */
16function formatAuthUrl(protocol, repositoryUrl, gitCredentials) {
17 const [match, auth, host, basePort, path] =
18 /^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
19 const {port, hostname, ...parsed} = parse(
20 match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
21 );
22
23 return format({
24 ...parsed,
25 auth: gitCredentials,
26 host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
27 protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
28 });
29}
30
31/**
32 * Verify authUrl by calling git.verifyAuth, but don't throw on failure
33 *
34 * @param {Object} context semantic-release context.
35 * @param {String} authUrl Repository URL to verify
36 *
37 * @return {String} The authUrl as is if the connection was successfull, null otherwise
38 */
39async function ensureValidAuthUrl({cwd, env, branch}, authUrl) {
40 try {
41 await verifyAuth(authUrl, branch.name, {cwd, env});
42 return authUrl;
43 } catch (error) {
44 debug(error);
45 return null;
46 }
47}
48
49/**
50 * Determine the the git repository URL to use to push, either:
51 * - The `repositoryUrl` as is if allowed to push
52 * - The `repositoryUrl` converted to `https` or `http` with Basic Authentication
53 *
54 * In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
55 *
56 * @param {Object} context semantic-release context.
57 *
58 * @return {String} The formatted Git repository URL.
59 */
60module.exports = async (context) => {
61 const {cwd, env, branch} = context;
62 const GIT_TOKENS = {
63 GIT_CREDENTIALS: undefined,
64 GH_TOKEN: undefined,
65 // GitHub Actions require the "x-access-token:" prefix for git access
66 // https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation
67 GITHUB_TOKEN: isNil(env.GITHUB_ACTION) ? undefined : 'x-access-token:',
68 GL_TOKEN: 'gitlab-ci-token:',
69 GITLAB_TOKEN: 'gitlab-ci-token:',
70 BB_TOKEN: 'x-token-auth:',
71 BITBUCKET_TOKEN: 'x-token-auth:',
72 BB_TOKEN_BASIC_AUTH: '',
73 BITBUCKET_TOKEN_BASIC_AUTH: '',
74 };
75
76 let {repositoryUrl} = context.options;
77 const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
78 const {protocol, ...parsed} = parse(repositoryUrl);
79
80 if (info && info.getDefaultRepresentation() === 'shortcut') {
81 // Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
82 repositoryUrl = info.https();
83 } else if (protocol && protocol.includes('http')) {
84 // Replace `git+https` and `git+http` with `https` or `http`
85 repositoryUrl = format({...parsed, protocol: protocol.includes('https') ? 'https' : 'http', href: null});
86 }
87
88 // Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
89 try {
90 debug('Verifying ssh auth by attempting to push to %s', repositoryUrl);
91 await verifyAuth(repositoryUrl, branch.name, {cwd, env});
92 } catch {
93 debug('SSH key auth failed, falling back to https.');
94 const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar]));
95
96 // Skip verification if there is no ambiguity on which env var to use for authentication
97 if (envVars.length === 1) {
98 const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`;
99 return formatAuthUrl(protocol, repositoryUrl, gitCredentials);
100 }
101
102 if (envVars.length > 1) {
103 debug(`Found ${envVars.length} credentials in environment, trying all of them`);
104
105 const candidateRepositoryUrls = [];
106 for (const envVar of envVars) {
107 const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`;
108 const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials);
109 candidateRepositoryUrls.push(ensureValidAuthUrl(context, authUrl));
110 }
111
112 const validRepositoryUrls = await Promise.all(candidateRepositoryUrls);
113 const chosenAuthUrlIndex = validRepositoryUrls.findIndex((url) => url !== null);
114 if (chosenAuthUrlIndex > -1) {
115 debug(`Using "${envVars[chosenAuthUrlIndex]}" to authenticate`);
116 return validRepositoryUrls[chosenAuthUrlIndex];
117 }
118 }
119 }
120
121 return repositoryUrl;
122};