Skip to content

Commit 2bf3771

Browse files
authored
fix: use valid git credentials when multiple are provided (#1669)
1 parent 77a75f0 commit 2bf3771

File tree

2 files changed

+107
-16
lines changed

2 files changed

+107
-16
lines changed

lib/get-git-auth-url.js

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,48 @@ const hostedGitInfo = require('hosted-git-info');
44
const {verifyAuth} = require('./git');
55
const debug = require('debug')('semantic-release:get-git-auth-url');
66

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+
*/
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+
749
/**
850
* Determine the the git repository URL to use to push, either:
951
* - The `repositoryUrl` as is if allowed to push
@@ -15,7 +57,8 @@ const debug = require('debug')('semantic-release:get-git-auth-url');
1557
*
1658
* @return {String} The formatted Git repository URL.
1759
*/
18-
module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
60+
module.exports = async (context) => {
61+
const {cwd, env, branch} = context;
1962
const GIT_TOKENS = {
2063
GIT_CREDENTIALS: undefined,
2164
GH_TOKEN: undefined,
@@ -30,6 +73,7 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
3073
BITBUCKET_TOKEN_BASIC_AUTH: '',
3174
};
3275

76+
let {repositoryUrl} = context.options;
3377
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
3478
const {protocol, ...parsed} = parse(repositoryUrl);
3579

@@ -47,24 +91,30 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
4791
await verifyAuth(repositoryUrl, branch.name, {cwd, env});
4892
} catch (_) {
4993
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+
}
50101

51-
const envVar = Object.keys(GIT_TOKENS).find((envVar) => !isNil(env[envVar]));
52-
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
102+
if (envVars.length > 1) {
103+
debug(`Found ${envVars.length} credentials in environment, trying all of them`);
53104

54-
if (gitCredentials) {
55-
// If credentials are set via environment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
56-
const [match, auth, host, basePort, path] =
57-
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
58-
const {port, hostname, ...parsed} = parse(
59-
match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
60-
);
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+
}
61111

62-
return format({
63-
...parsed,
64-
auth: gitCredentials,
65-
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
66-
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
67-
});
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+
}
68118
}
69119
}
70120

test/integration.test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {writeJson, readJson} = require('fs-extra');
66
const execa = require('execa');
77
const {WritableStreamBuffer} = require('stream-buffers');
88
const delay = require('delay');
9+
const getAuthUrl = require('../lib/get-git-auth-url');
910
const {SECRET_REPLACEMENT} = require('../lib/definitions/constants');
1011
const {
1112
gitHead,
@@ -656,3 +657,43 @@ test('Hide sensitive environment variable values from the logs', async (t) => {
656657
t.regex(stderr, new RegExp(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`));
657658
t.regex(stderr, new RegExp(`Throw error: Exposing ${escapeRegExp(SECRET_REPLACEMENT)}`));
658659
});
660+
661+
test('Use the valid git credentials when multiple are provided', async (t) => {
662+
const {cwd, authUrl} = await gitbox.createRepo('test-auth');
663+
664+
t.is(
665+
await getAuthUrl({
666+
cwd,
667+
env: {
668+
GITHUB_TOKEN: 'dummy',
669+
GITLAB_TOKEN: 'trash',
670+
BB_TOKEN_BASIC_AUTH: gitbox.gitCredential,
671+
GIT_ASKPASS: 'echo',
672+
GIT_TERMINAL_PROMPT: 0,
673+
},
674+
branch: {name: 'master'},
675+
options: {repositoryUrl: 'http://toto@localhost:2080/git/test-auth.git'},
676+
}),
677+
authUrl
678+
);
679+
});
680+
681+
test('Use the repository URL as is if none of the given git credentials are valid', async (t) => {
682+
const {cwd} = await gitbox.createRepo('test-invalid-auth');
683+
const dummyUrl = 'http://toto@localhost:2080/git/test-auth.git';
684+
685+
t.is(
686+
await getAuthUrl({
687+
cwd,
688+
env: {
689+
GITHUB_TOKEN: 'dummy',
690+
GITLAB_TOKEN: 'trash',
691+
GIT_ASKPASS: 'echo',
692+
GIT_TERMINAL_PROMPT: 0,
693+
},
694+
branch: {name: 'master'},
695+
options: {repositoryUrl: dummyUrl},
696+
}),
697+
dummyUrl
698+
);
699+
});

0 commit comments

Comments
 (0)