diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index 66685a10e25e68ae1b141663f70c4029a45ea8e4..a4a02f2e354a6a39e4d346a42e969900f1ba59e7 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -238,6 +238,16 @@ Before the first commit in a repository, Renovate will: The `git` commands are run locally in the cloned repo instead of globally. This reduces the chance of unintended consequences with global Git configs on shared systems. +## gitUrl + +Override the default resolution for git remote, e.g. to switch GitLab from HTTPS to SSH-based. Currently works for GitLab only. + +Possible values: + +- `default`: use HTTP URLs provided by the platform for Git +- `ssh`: use SSH URLs provided by the platform for Git +- `endpoint`: ignore URLs provided by the platform and use the configured endpoint directly + ## logContext `logContext` is included with each log entry only if `logFormat="json"` - it is not included in the pretty log output. diff --git a/docs/usage/self-hosted-experimental.md b/docs/usage/self-hosted-experimental.md index 2f00716900c3b6269365ef2dc5a4ef8e863ac827..556152ca162a59a287b6d374c2a424d17306230f 100644 --- a/docs/usage/self-hosted-experimental.md +++ b/docs/usage/self-hosted-experimental.md @@ -14,10 +14,6 @@ We do not follow Semantic Versioning for any experimental variables. These variables may be removed or have their behavior changed in **any** version. We will try to keep breakage to a minimum, but make no guarantees that a experimental variable will keep working. -## GITLAB_IGNORE_REPO_URL - -If set to any value, Renovate will ignore the Project's `http_url_to_repo` value and instead construct the Git URL manually. - ## RENOVATE_CACHE_NPM_MINUTES If set to any integer, Renovate will use this integer instead of the default npm cache time (15 minutes) for the npm datasource. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index 4017df1c3613ceae91f3fae0c3934ad8f79df786..b796b0990ddd569b8401dc8914cc3f83b404cccf 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -2030,6 +2030,16 @@ const options: RenovateOptions[] = [ type: 'boolean', default: true, }, + { + name: 'gitUrl', + description: + 'Overrides the default resolution for git remote, e.g. to switch GitLab from HTTPS to SSH-based.', + type: 'string', + allowedValues: ['default', 'ssh', 'endpoint'], + default: 'default', + stage: 'repository', + globalOnly: true, + }, ]; export function getOptions(): RenovateOptions[] { diff --git a/lib/constants/error-messages.ts b/lib/constants/error-messages.ts index eec1bbff1ecdff253c46dd0b3b7a63c47dc4b43f..a71b6ae9fa39fbf28a49b8411ddc97f858d279a0 100644 --- a/lib/constants/error-messages.ts +++ b/lib/constants/error-messages.ts @@ -15,6 +15,7 @@ export const CONFIG_VALIDATION = 'config-validation'; export const CONFIG_PRESETS_INVALID = 'config-presets-invalid'; export const CONFIG_SECRETS_EXPOSED = 'config-secrets-exposed'; export const CONFIG_SECRETS_INVALID = 'config-secrets-invalid'; +export const CONFIG_GIT_URL_UNAVAILABLE = 'config-git-url-unavailable'; // Repository Errors - causes repo to be considered as disabled export const REPOSITORY_ACCESS_FORBIDDEN = 'forbidden'; diff --git a/lib/platform/gitlab/__snapshots__/index.spec.ts.snap b/lib/platform/gitlab/__snapshots__/index.spec.ts.snap index 8bd6f87c7dd2f1fe696765f76267850cedbe2df8..5d4c684615dd188981291825b4ddcd9a02be1266 100644 --- a/lib/platform/gitlab/__snapshots__/index.spec.ts.snap +++ b/lib/platform/gitlab/__snapshots__/index.spec.ts.snap @@ -2653,6 +2653,39 @@ Array [ ] `; +exports[`platform/gitlab/index initRepo should use ssh_url_to_repo if gitUrl is set to ssh 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "Bearer abc123", + "host": "gitlab.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/some%2Frepo%2Fproject", + }, +] +`; + +exports[`platform/gitlab/index initRepo should use ssh_url_to_repo if gitUrl is set to ssh 2`] = ` +Array [ + Array [ + Object { + "cloneSubmodules": undefined, + "defaultBranch": "master", + "gitAuthorEmail": undefined, + "gitAuthorName": undefined, + "ignorePrAuthor": undefined, + "mergeMethod": "merge", + "repository": "some%2Frepo%2Fproject", + "url": "ssh://git@gitlab.com/some%2Frepo%2Fproject.git", + }, + ], +] +`; + exports[`platform/gitlab/index initRepo should fall back respecting when GITLAB_IGNORE_REPO_URL is set 1`] = ` Array [ Array [ diff --git a/lib/platform/gitlab/index.spec.ts b/lib/platform/gitlab/index.spec.ts index 66fc051eca391a96c067ee740e613e140b332297..9a27a2399a49b158e897f99d795b7ae0e46a5f44 100644 --- a/lib/platform/gitlab/index.spec.ts +++ b/lib/platform/gitlab/index.spec.ts @@ -285,6 +285,23 @@ describe('platform/gitlab/index', () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('should use ssh_url_to_repo if gitUrl is set to ssh', async () => { + httpMock + .scope(gitlabApiHost) + .get('/api/v4/projects/some%2Frepo%2Fproject') + .reply(200, { + default_branch: 'master', + http_url_to_repo: `https://gitlab.com/some%2Frepo%2Fproject.git`, + ssh_url_to_repo: `ssh://git@gitlab.com/some%2Frepo%2Fproject.git`, + }); + await gitlab.initRepo({ + repository: 'some/repo/project', + gitUrl: 'ssh', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(git.initRepo.mock.calls).toMatchSnapshot(); + }); + it('should fall back respecting when GITLAB_IGNORE_REPO_URL is set', async () => { process.env.GITLAB_IGNORE_REPO_URL = 'true'; const selfHostedUrl = 'http://mycompany.com/gitlab'; diff --git a/lib/platform/gitlab/index.ts b/lib/platform/gitlab/index.ts index 33ab186ef322621ac9f4920acf3ea55ba12d891b..bd2e7afd6d2250b63bbe7d4bbfdf47b3ef1e50d2 100644 --- a/lib/platform/gitlab/index.ts +++ b/lib/platform/gitlab/index.ts @@ -4,6 +4,7 @@ import delay from 'delay'; import pAll from 'p-all'; import { lt } from 'semver'; import { + CONFIG_GIT_URL_UNAVAILABLE, PLATFORM_AUTHENTICATION_ERROR, REPOSITORY_ACCESS_FORBIDDEN, REPOSITORY_ARCHIVED, @@ -30,6 +31,7 @@ import type { EnsureCommentRemovalConfig, EnsureIssueConfig, FindPRConfig, + GitUrlOption, Issue, MergePRConfig, PlatformParams, @@ -162,11 +164,62 @@ export async function getJsonFile( return JSON.parse(raw); } +function getRepoUrl( + repository: string, + gitUrl: GitUrlOption | undefined, + res: HttpResponse<RepoResponse> +): string { + if (gitUrl === 'ssh') { + if (!res.body.ssh_url_to_repo) { + throw new Error(CONFIG_GIT_URL_UNAVAILABLE); + } + logger.debug({ url: res.body.ssh_url_to_repo }, `using ssh URL`); + return res.body.ssh_url_to_repo; + } + + const opts = hostRules.find({ + hostType: defaults.hostType, + url: defaults.endpoint, + }); + + if ( + gitUrl === 'endpoint' || + process.env.GITLAB_IGNORE_REPO_URL || + res.body.http_url_to_repo === null + ) { + if (res.body.http_url_to_repo === null) { + logger.debug('no http_url_to_repo found. Falling back to old behaviour.'); + } + if (process.env.GITLAB_IGNORE_REPO_URL) { + logger.warn( + 'GITLAB_IGNORE_REPO_URL environment variable is deprecated. Please use "gitUrl" option.' + ); + } + + const { protocol, host, pathname } = parseUrl(defaults.endpoint); + const newPathname = pathname.slice(0, pathname.indexOf('/api')); + const url = URL.format({ + protocol: protocol.slice(0, -1) || 'https', + auth: 'oauth2:' + opts.token, + host, + pathname: newPathname + '/' + repository + '.git', + }); + logger.debug({ url }, 'using URL based on configured endpoint'); + return url; + } + + logger.debug({ url: res.body.http_url_to_repo }, `using http URL`); + const repoUrl = URL.parse(`${res.body.http_url_to_repo}`); + repoUrl.auth = 'oauth2:' + opts.token; + return URL.format(repoUrl); +} + // Initialize GitLab by getting base branch export async function initRepo({ repository, cloneSubmodules, ignorePrAuthor, + gitUrl, }: RepoParams): Promise<RepoResult> { config = {} as any; config.repository = urlEscape(repository); @@ -220,31 +273,7 @@ export async function initRepo({ logger.debug(`${repository} default branch = ${config.defaultBranch}`); delete config.prList; logger.debug('Enabling Git FS'); - const opts = hostRules.find({ - hostType: defaults.hostType, - url: defaults.endpoint, - }); - let url: string; - if ( - process.env.GITLAB_IGNORE_REPO_URL || - res.body.http_url_to_repo === null - ) { - logger.debug('no http_url_to_repo found. Falling back to old behaviour.'); - const { protocol, host, pathname } = parseUrl(defaults.endpoint); - const newPathname = pathname.slice(0, pathname.indexOf('/api')); - url = URL.format({ - protocol: protocol.slice(0, -1) || 'https', - auth: 'oauth2:' + opts.token, - host, - pathname: newPathname + '/' + repository + '.git', - }); - logger.debug({ url }, 'using URL based on configured endpoint'); - } else { - logger.debug(`${repository} http URL = ${res.body.http_url_to_repo}`); - const repoUrl = URL.parse(`${res.body.http_url_to_repo}`); - repoUrl.auth = 'oauth2:' + opts.token; - url = URL.format(repoUrl); - } + const url = getRepoUrl(repository, gitUrl, res); await git.initRepo({ ...config, url, diff --git a/lib/platform/gitlab/types.ts b/lib/platform/gitlab/types.ts index 4a59858488afc6770e94c2f8dd356a6e70bba237..aaffe7386ec5715bcee1c16ba937bf477d35dbc5 100644 --- a/lib/platform/gitlab/types.ts +++ b/lib/platform/gitlab/types.ts @@ -47,6 +47,7 @@ export interface RepoResponse { mirror: boolean; default_branch: string; empty_repo: boolean; + ssh_url_to_repo: string; http_url_to_repo: string; forked_from_project: boolean; repository_access_level: 'disabled' | 'private' | 'enabled'; diff --git a/lib/platform/types.ts b/lib/platform/types.ts index 020247e95576e8059837304c09b370d5d3df99e3..04981cff72e02a117779b5edf2cede8c9258a96e 100644 --- a/lib/platform/types.ts +++ b/lib/platform/types.ts @@ -27,9 +27,12 @@ export interface RepoResult { isFork: boolean; } +export type GitUrlOption = 'default' | 'ssh' | 'endpoint'; + export interface RepoParams { repository: string; endpoint?: string; + gitUrl?: GitUrlOption; forkMode?: string; forkToken?: string; includeForks?: boolean;