1 | const {parse, format} = require('url');
|
2 | const {isNil} = require('lodash');
|
3 | const hostedGitInfo = require('hosted-git-info');
|
4 | const {verifyAuth} = require('./git');
|
5 | const debug = require('debug')('semantic-release:get-git-auth-url');
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 | function 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 | */
|
39 | async 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 | */
|
60 | module.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 | };
|