From 75798754151ef4078eff9ba0a57b8f861678c718 Mon Sep 17 00:00:00 2001 From: Michael Todorovic Date: Fri, 5 Nov 2021 14:12:47 +0100 Subject: [PATCH] feat: support preset versioning with git tags (#11565) Co-authored-by: Rhys Arkins Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese Co-authored-by: Sergei Zharinov --- docs/development/shareable-configs.md | 50 +++++++++++++--- docs/usage/config-presets.md | 60 ++++++++++++++----- .../gitea/__snapshots__/index.spec.ts.snap | 32 ++++++++++ lib/config/presets/gitea/index.spec.ts | 42 ++++++++++++- lib/config/presets/gitea/index.ts | 20 +++++-- .../github/__snapshots__/index.spec.ts.snap | 32 ++++++++++ lib/config/presets/github/index.spec.ts | 46 +++++++++++++- lib/config/presets/github/index.ts | 24 ++++++-- .../gitlab/__snapshots__/index.spec.ts.snap | 45 ++++++++++++++ lib/config/presets/gitlab/index.spec.ts | 48 +++++++++++++++ lib/config/presets/gitlab/index.ts | 34 ++++++++--- lib/config/presets/index.spec.ts | 8 +-- lib/config/presets/index.ts | 47 +++++++++++---- .../local/__snapshots__/index.spec.ts.snap | 57 ++++++++++++++++++ lib/config/presets/local/index.spec.ts | 49 +++++++++++++++ lib/config/presets/local/index.ts | 4 +- lib/config/presets/types.ts | 6 +- lib/config/presets/util.ts | 10 +++- 18 files changed, 553 insertions(+), 61 deletions(-) diff --git a/docs/development/shareable-configs.md b/docs/development/shareable-configs.md index 1412603a9036a8..1add3fc2f1f00a 100644 --- a/docs/development/shareable-configs.md +++ b/docs/development/shareable-configs.md @@ -50,12 +50,44 @@ If you use a non-scoped config, you must use a preset name! In general, GitHub, GitLab or Gitea-based preset hosting is easier than npm because you avoid the "publish" step - simply commit preset code to the default branch and it will be picked up by Renovate the next time it runs. An additional benefit of using source code hosting is that the same token/authentication can be reused by Renovate in case you want to make your config private. -| name | example use | preset | resolves as | filename | -| ----------------------- | -------------------- | --------- | ------------------------------------ | -------------- | -| GitHub default | `github>abc/foo` | `default` | `https://github.com/abc/foo` | `default.json` | -| GitHub with preset name | `github>abc/foo:xyz` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | -| GitLab default | `gitlab>abc/foo` | `default` | `https://gitlab.com/abc/foo` | `default.json` | -| GitLab with preset name | `gitlab>abc/foo:xyz` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | -| Gitea default | `gitea>abc/foo` | `default` | `https://gitea.com/abc/foo` | `default.json` | -| Gitea with preset name | `gitea>abc/foo:xyz` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | -| Local default | `local>abc/foo` | `default` | `https://github.company.com/abc/foo` | `default.json` | +You can set a Git tag (like a SemVer) to use a specific release of your shared config. + +#### GitHub + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------- | ------------------------------- | --------- | ---------------------------- | --------------- | -------------- | +| GitHub default | `github>abc/foo` | `default` | `https://github.com/abc/foo` | `default.json` | Default branch | +| GitHub with preset name | `github>abc/foo:xyz` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | Default branch | +| GitHub default with a tag | `github>abc/foo#1.5.4` | `default` | `https://github.com/abc/foo` | `default.json` | `1.5.4` | +| GitHub with preset name with a tag | `github>abc/foo:xyz#1.5.4` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | `1.5.4` | +| GitHub with preset name and path with a tag | `github>abc/foo:path/xyz#1.5.4` | `xyz` | `https://github.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +#### GitLab + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------- | ------------------------------- | --------- | ---------------------------- | --------------- | -------------- | +| GitLab default | `gitlab>abc/foo` | `default` | `https://gitlab.com/abc/foo` | `default.json` | Default branch | +| GitLab with preset name | `gitlab>abc/foo:xyz` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | Default branch | +| GitLab default with a tag | `gitlab>abc/foo#1.5.4` | `default` | `https://gitlab.com/abc/foo` | `default.json` | `1.5.4` | +| GitLab with preset name with a tag | `gitlab>abc/foo:xyz#1.5.4` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | `1.5.4` | +| GitLab with preset name and path with a tag | `gitlab>abc/foo:path/xyz#1.5.4` | `xyz` | `https://gitlab.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +#### Gitea + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------ | ------------------------------ | --------- | --------------------------- | --------------- | -------------- | +| Gitea default | `gitea>abc/foo` | `default` | `https://gitea.com/abc/foo` | `default.json` | Default branch | +| Gitea with preset name | `gitea>abc/foo:xyz` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | Default branch | +| Gitea default with a tag | `gitea>abc/foo#1.5.4` | `default` | `https://gitea.com/abc/foo` | `default.json` | `1.5.4` | +| Gitea with preset name with a tag | `gitea>abc/foo:xyz#1.5.4` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | `1.5.4` | +| Gitea with preset name and path with a tag | `gitea>abc/foo:path/xyz#1.5.4` | `xyz` | `https://gitea.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +#### Self-hosted Git + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------ | ------------------------------ | --------- | ------------------------------------ | --------------- | -------------- | +| Local default | `local>abc/foo` | `default` | `https://github.company.com/abc/foo` | `default.json` | Default branch | +| Local with preset path | `local>abc/foo:path/xyz` | `default` | `https://github.company.com/abc/foo` | `path/xyz.json` | Default branch | +| Local default with a tag | `local>abc/foo#1.5.4` | `default` | `https://github.company.com/abc/foo` | `default.json` | `1.5.4` | +| Local with preset name with a tag | `local>abc/foo:xyz#1.5.4` | `default` | `https://github.company.com/abc/foo` | `xyz.json` | `1.5.4` | +| Local with preset name and path with a tag | `local>abc/foo:path/xyz#1.5.4` | `default` | `https://github.company.com/abc/foo` | `path/xyz.json` | `1.5.4` | diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md index a1cccb4519e4d4..f1e5d450e3741d 100644 --- a/docs/usage/config-presets.md +++ b/docs/usage/config-presets.md @@ -38,22 +38,54 @@ In order to achieve these goals, preset configs allow for a very modular approac ## Preset Hosting In general, GitHub, GitLab or Gitea-based preset hosting is easier than npm because you avoid the "publish" step - simply commit preset code to the default branch and it will be picked up by Renovate the next time it runs. + An additional benefit of using source code hosting is that the same token/authentication can be reused by Renovate in case you want to make your config private. -| name | example use | preset | resolves as | filename | -| ----------------------- | -------------------------- | --------- | ------------------------------------ | --------------- | -| GitHub default | `github>abc/foo` | `default` | `https://github.com/abc/foo` | `default.json` | -| GitHub with preset name | `github>abc/foo:xyz` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | -| GitHub with preset path | `github>abc/foo//path/xyz` | `xyz` | `https://github.com/abc/foo` | `path/xyz.json` | -| GitLab default | `gitlab>abc/foo` | `default` | `https://gitlab.com/abc/foo` | `default.json` | -| GitLab with preset name | `gitlab>abc/foo:xyz` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | -| GitLab with preset path | `gitlab>abc/foo//path/xyz` | `xyz` | `https://gitlab.com/abc/foo` | `path/xyz.json` | -| Gitea default | `gitea>abc/foo` | `default` | `https://gitea.com/abc/foo` | `default.json` | -| Gitea with preset name | `gitea>abc/foo:xyz` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | -| Local default | `local>abc/foo` | `default` | `https://github.company.com/abc/foo` | `default.json` | -| Local with preset path | `local>abc/foo//path/xyz` | `xyz` | `https://github.company.com/abc/foo` | `path/xyz.json` | - -Note that you can't combine the path and sub-preset syntaxes (i.e. anything in the form `provider>owner/repo//path/to/file:subsubpreset`) is not supported. One workaround is to use distinct files instead of sub-presets. +You can set a Git tag (like a SemVer) to use a specific release of your shared config. + +### GitHub + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------- | ------------------------------- | --------- | ---------------------------- | --------------- | -------------- | +| GitHub default | `github>abc/foo` | `default` | `https://github.com/abc/foo` | `default.json` | Default branch | +| GitHub with preset name | `github>abc/foo:xyz` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | Default branch | +| GitHub default with a tag | `github>abc/foo#1.5.4` | `default` | `https://github.com/abc/foo` | `default.json` | `1.5.4` | +| GitHub with preset name with a tag | `github>abc/foo:xyz#1.5.4` | `xyz` | `https://github.com/abc/foo` | `xyz.json` | `1.5.4` | +| GitHub with preset name and path with a tag | `github>abc/foo:path/xyz#1.5.4` | `xyz` | `https://github.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +### GitLab + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------- | ------------------------------- | --------- | ---------------------------- | --------------- | -------------- | +| GitLab default | `gitlab>abc/foo` | `default` | `https://gitlab.com/abc/foo` | `default.json` | Default branch | +| GitLab with preset name | `gitlab>abc/foo:xyz` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | Default branch | +| GitLab default with a tag | `gitlab>abc/foo#1.5.4` | `default` | `https://gitlab.com/abc/foo` | `default.json` | `1.5.4` | +| GitLab with preset name with a tag | `gitlab>abc/foo:xyz#1.5.4` | `xyz` | `https://gitlab.com/abc/foo` | `xyz.json` | `1.5.4` | +| GitLab with preset name and path with a tag | `gitlab>abc/foo:path/xyz#1.5.4` | `xyz` | `https://gitlab.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +### Gitea + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------ | ------------------------------ | --------- | --------------------------- | --------------- | -------------- | +| Gitea default | `gitea>abc/foo` | `default` | `https://gitea.com/abc/foo` | `default.json` | Default branch | +| Gitea with preset name | `gitea>abc/foo:xyz` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | Default branch | +| Gitea default with a tag | `gitea>abc/foo#1.5.4` | `default` | `https://gitea.com/abc/foo` | `default.json` | `1.5.4` | +| Gitea with preset name with a tag | `gitea>abc/foo:xyz#1.5.4` | `xyz` | `https://gitea.com/abc/foo` | `xyz.json` | `1.5.4` | +| Gitea with preset name and path with a tag | `gitea>abc/foo:path/xyz#1.5.4` | `xyz` | `https://gitea.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +### Self-hosted Git + +| name | example use | preset | resolves as | filename | Git tag | +| ------------------------------------------ | ------------------------------ | --------- | ------------------------------------ | --------------- | -------------- | +| Local default | `local>abc/foo` | `default` | `https://github.company.com/abc/foo` | `default.json` | Default branch | +| Local with preset path | `local>abc/foo:path/xyz` | `xyz` | `https://github.company.com/abc/foo` | `path/xyz.json` | Default branch | +| Local default with a tag | `local>abc/foo#1.5.4` | `default` | `https://github.company.com/abc/foo` | `default.json` | `1.5.4` | +| Local with preset name with a tag | `local>abc/foo:xyz#1.5.4` | `xyz` | `https://github.company.com/abc/foo` | `xyz.json` | `1.5.4` | +| Local with preset name and path with a tag | `local>abc/foo:path/xyz#1.5.4` | `xyz` | `https://github.company.com/abc/foo` | `path/xyz.json` | `1.5.4` | + +Note that you can't combine the path and sub-preset syntaxes. +This means that anything in the form `provider>owner/repo//path/to/file:subsubpreset` is not supported. +One workaround is to use distinct files instead of sub-presets. ## Example configs diff --git a/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap index 85b877cd000bd0..2459bdead50b7f 100644 --- a/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap @@ -187,6 +187,22 @@ Array [ ] `; +exports[`config/presets/gitea/index getPresetFromEndpoint() uses custom endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc", + "host": "api.gitea.example.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.gitea.example.org/repos/some/repo/contents/default.json?ref=someTag", + }, +] +`; + exports[`config/presets/gitea/index getPresetFromEndpoint() uses default endpoint 1`] = ` Array [ Object { @@ -202,3 +218,19 @@ Array [ }, ] `; + +exports[`config/presets/gitea/index getPresetFromEndpoint() uses default endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc", + "host": "gitea.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitea.com/api/v1/repos/some/repo/contents/default.json?ref=someTag", + }, +] +`; diff --git a/lib/config/presets/gitea/index.spec.ts b/lib/config/presets/gitea/index.spec.ts index c9394a3a6632c9..9a4da378f8468a 100644 --- a/lib/config/presets/gitea/index.spec.ts +++ b/lib/config/presets/gitea/index.spec.ts @@ -30,7 +30,8 @@ describe('config/presets/gitea/index', () => { const res = await gitea.fetchJSONFile( 'some/repo', 'some-filename.json', - giteaApiHost + giteaApiHost, + null ); expect(res).toEqual({ from: 'api' }); expect(httpMock.getTrace()).toMatchSnapshot(); @@ -205,5 +206,44 @@ describe('config/presets/gitea/index', () => { ).toEqual({ from: 'api' }); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('uses default endpoint with a tag', async () => { + httpMock + .scope(giteaApiHost) + .get(`${basePath}/default.json?ref=someTag`) + .reply(200, { + content: Buffer.from('{"from":"api"}').toString('base64'), + }); + expect( + await gitea.getPresetFromEndpoint( + 'some/repo', + 'default', + undefined, + giteaApiHost, + 'someTag' + ) + ).toEqual({ from: 'api' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('uses custom endpoint with a tag', async () => { + httpMock + .scope('https://api.gitea.example.org') + .get(`${basePath}/default.json?ref=someTag`) + .reply(200, { + content: Buffer.from('{"from":"api"}').toString('base64'), + }); + expect( + await gitea + .getPresetFromEndpoint( + 'some/repo', + 'default', + undefined, + 'https://api.gitea.example.org', + 'someTag' + ) + .catch(() => ({ from: 'api' })) + ).toEqual({ from: 'api' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); }); }); diff --git a/lib/config/presets/gitea/index.ts b/lib/config/presets/gitea/index.ts index 3c4f560bb2cbd4..8e93d2d7d56184 100644 --- a/lib/config/presets/gitea/index.ts +++ b/lib/config/presets/gitea/index.ts @@ -16,11 +16,14 @@ export const Endpoint = 'https://gitea.com/api/v1/'; export async function fetchJSONFile( repo: string, fileName: string, - endpoint: string + endpoint: string, + packageTag?: string ): Promise { let res: RepoContents; try { - res = await getRepoContents(repo, fileName, null, { baseUrl: endpoint }); + res = await getRepoContents(repo, fileName, packageTag, { + baseUrl: endpoint, + }); } catch (err) { // istanbul ignore if: not testable with nock if (err instanceof ExternalHostError) { @@ -45,13 +48,15 @@ export function getPresetFromEndpoint( pkgName: string, filePreset: string, presetPath: string, - endpoint = Endpoint + endpoint = Endpoint, + packageTag?: string ): Promise { return fetchPreset({ pkgName, filePreset, presetPath, endpoint, + packageTag, fetch: fetchJSONFile, }); } @@ -60,6 +65,13 @@ export function getPreset({ packageName: pkgName, presetName = 'default', presetPath, + packageTag = null, }: PresetConfig): Promise { - return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); + return getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + Endpoint, + packageTag + ); } diff --git a/lib/config/presets/github/__snapshots__/index.spec.ts.snap b/lib/config/presets/github/__snapshots__/index.spec.ts.snap index b87f96fba1bb3f..695c8859337378 100644 --- a/lib/config/presets/github/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/github/__snapshots__/index.spec.ts.snap @@ -187,6 +187,22 @@ Array [ ] `; +exports[`config/presets/github/index getPresetFromEndpoint() uses custom endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc", + "host": "api.github.example.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.github.example.org/repos/some/repo/contents/default.json?ref=someTag", + }, +] +`; + exports[`config/presets/github/index getPresetFromEndpoint() uses default endpoint 1`] = ` Array [ Object { @@ -202,3 +218,19 @@ Array [ }, ] `; + +exports[`config/presets/github/index getPresetFromEndpoint() uses default endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "authorization": "token abc", + "host": "api.github.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://api.github.com/repos/some/repo/contents/default.json?ref=someTag", + }, +] +`; diff --git a/lib/config/presets/github/index.spec.ts b/lib/config/presets/github/index.spec.ts index 1ae4e70822a91c..f4f7db5c7984c1 100644 --- a/lib/config/presets/github/index.spec.ts +++ b/lib/config/presets/github/index.spec.ts @@ -28,7 +28,8 @@ describe('config/presets/github/index', () => { const res = await github.fetchJSONFile( 'some/repo', 'some-filename.json', - githubApiHost + githubApiHost, + undefined ); expect(res).toEqual({ from: 'api' }); expect(httpMock.getTrace()).toMatchSnapshot(); @@ -197,7 +198,48 @@ describe('config/presets/github/index', () => { 'some/repo', 'default', undefined, - 'https://api.github.example.org' + 'https://api.github.example.org', + undefined + ) + .catch(() => ({ from: 'api' })) + ).toEqual({ from: 'api' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('uses default endpoint with a tag', async () => { + httpMock + .scope(githubApiHost) + .get(`${basePath}/default.json?ref=someTag`) + .reply(200, { + content: Buffer.from('{"from":"api"}').toString('base64'), + }); + expect( + await github.getPresetFromEndpoint( + 'some/repo', + 'default', + undefined, + githubApiHost, + 'someTag' + ) + ).toEqual({ from: 'api' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('uses custom endpoint with a tag', async () => { + httpMock + .scope('https://api.github.example.org') + .get(`${basePath}/default.json?ref=someTag`) + .reply(200, { + content: Buffer.from('{"from":"api"}').toString('base64'), + }); + expect( + await github + .getPresetFromEndpoint( + 'some/repo', + 'default', + undefined, + 'https://api.github.example.org', + 'someTag' ) .catch(() => ({ from: 'api' })) ).toEqual({ from: 'api' }); diff --git a/lib/config/presets/github/index.ts b/lib/config/presets/github/index.ts index 0f1a9be5cb0d09..70ca99cfac29ae 100644 --- a/lib/config/presets/github/index.ts +++ b/lib/config/presets/github/index.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import { GithubHttp } from '../../../util/http/github'; @@ -15,9 +16,15 @@ const http = new GithubHttp(); export async function fetchJSONFile( repo: string, fileName: string, - endpoint: string + endpoint: string, + packageTag?: string ): Promise { - const url = `${endpoint}repos/${repo}/contents/${fileName}`; + let ref = ''; + if (is.nonEmptyString(packageTag)) { + ref = `?ref=${packageTag}`; + } + const url = `${endpoint}repos/${repo}/contents/${fileName}${ref}`; + logger.trace({ url }, `Preset URL`); let res: { body: { content: string } }; try { res = await http.getJson(url); @@ -45,13 +52,15 @@ export function getPresetFromEndpoint( pkgName: string, filePreset: string, presetPath: string, - endpoint = Endpoint + endpoint = Endpoint, + packageTag?: string ): Promise { return fetchPreset({ pkgName, filePreset, presetPath, endpoint, + packageTag, fetch: fetchJSONFile, }); } @@ -60,6 +69,13 @@ export function getPreset({ packageName: pkgName, presetName = 'default', presetPath, + packageTag = null, }: PresetConfig): Promise { - return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); + return getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + Endpoint, + packageTag + ); } diff --git a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap index 9f357196079af1..cdd63e345cdad7 100644 --- a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap @@ -50,6 +50,21 @@ Array [ ] `; +exports[`config/presets/gitlab/index getPreset() should return the preset with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "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/repository/files/default.json/raw?ref=someTag", + }, +] +`; + exports[`config/presets/gitlab/index getPreset() throws EXTERNAL_HOST_ERROR 1`] = ` Array [ Object { @@ -135,6 +150,21 @@ Array [ ] `; +exports[`config/presets/gitlab/index getPresetFromEndpoint() uses custom endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "gitlab.example.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://gitlab.example.org/api/v4/projects/some%2Frepo/repository/files/some.json/raw?ref=someTag", + }, +] +`; + exports[`config/presets/gitlab/index getPresetFromEndpoint() uses default endpoint 1`] = ` Array [ Object { @@ -159,3 +189,18 @@ Array [ }, ] `; + +exports[`config/presets/gitlab/index getPresetFromEndpoint() uses default endpoint with a tag 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "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/repository/files/some.json/raw?ref=someTag", + }, +] +`; diff --git a/lib/config/presets/gitlab/index.spec.ts b/lib/config/presets/gitlab/index.spec.ts index 1ba88176c663a2..291dec613e3936 100644 --- a/lib/config/presets/gitlab/index.spec.ts +++ b/lib/config/presets/gitlab/index.spec.ts @@ -60,6 +60,20 @@ describe('config/presets/gitlab/index', () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('should return the preset with a tag', async () => { + httpMock + .scope(gitlabApiHost) + .get(`${basePath}/files/default.json/raw?ref=someTag`) + .reply(200, { foo: 'bar' }, {}); + + const content = await gitlab.getPreset({ + packageName: 'some/repo', + packageTag: 'someTag', + }); + expect(content).toEqual({ foo: 'bar' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('should query custom paths', async () => { httpMock .scope(gitlabApiHost) @@ -131,5 +145,39 @@ describe('config/presets/gitlab/index', () => { ).rejects.toThrow(PRESET_DEP_NOT_FOUND); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('uses default endpoint with a tag', async () => { + httpMock + .scope(gitlabApiHost) + .get(`${basePath}/files/some.json/raw?ref=someTag`) + .reply(200, { preset: { file: {} } }); + expect( + await gitlab.getPresetFromEndpoint( + 'some/repo', + 'some/preset/file', + undefined, + 'https://gitlab.com/api/v4', + 'someTag' + ) + ).toEqual({}); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('uses custom endpoint with a tag', async () => { + httpMock + .scope('https://gitlab.example.org') + .get(`${basePath}/files/some.json/raw?ref=someTag`) + .reply(200, { preset: { file: {} } }); + expect( + await gitlab.getPresetFromEndpoint( + 'some/repo', + 'some/preset/file', + undefined, + 'https://gitlab.example.org/api/v4', + 'someTag' + ) + ).toEqual({}); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); }); }); diff --git a/lib/config/presets/gitlab/index.ts b/lib/config/presets/gitlab/index.ts index 829ee1e0223a57..fbd45c5124e4ad 100644 --- a/lib/config/presets/gitlab/index.ts +++ b/lib/config/presets/gitlab/index.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import { logger } from '../../../logger'; import { ExternalHostError } from '../../../types/errors/external-host-error'; import type { GitLabBranch } from '../../../types/platform/gitlab'; @@ -30,17 +31,25 @@ async function getDefaultBranchName( export async function fetchJSONFile( repo: string, fileName: string, - endpoint: string + endpoint: string, + packageTag?: string ): Promise { let url = endpoint; + let ref = ''; try { const urlEncodedRepo = encodeURIComponent(repo); const urlEncodedPkgName = encodeURIComponent(fileName); - const defaultBranchName = await getDefaultBranchName( - urlEncodedRepo, - endpoint - ); - url += `projects/${urlEncodedRepo}/repository/files/${urlEncodedPkgName}/raw?ref=${defaultBranchName}`; + if (is.nonEmptyString(packageTag)) { + ref = `?ref=${packageTag}`; + } else { + const defaultBranchName = await getDefaultBranchName( + urlEncodedRepo, + endpoint + ); + ref = `?ref=${defaultBranchName}`; + } + url += `projects/${urlEncodedRepo}/repository/files/${urlEncodedPkgName}/raw${ref}`; + logger.trace({ url }, `Preset URL`); return (await gitlabApi.getJson(url)).body; } catch (err) { if (err instanceof ExternalHostError) { @@ -58,13 +67,15 @@ export function getPresetFromEndpoint( pkgName: string, presetName: string, presetPath: string, - endpoint = Endpoint + endpoint = Endpoint, + packageTag?: string ): Promise { return fetchPreset({ pkgName, filePreset: presetName, presetPath, endpoint, + packageTag, fetch: fetchJSONFile, }); } @@ -73,6 +84,13 @@ export function getPreset({ packageName: pkgName, presetPath, presetName = 'default', + packageTag = null, }: PresetConfig): Promise { - return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); + return getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + Endpoint, + packageTag + ); } diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts index 32763461fa32dc..c96efd8360884f 100644 --- a/lib/config/presets/index.spec.ts +++ b/lib/config/presets/index.spec.ts @@ -337,8 +337,8 @@ describe('config/presets/index', () => { ).toEqual({ packageName: 'some/repo', params: undefined, - presetName: 'somefile/somepreset', - presetPath: undefined, + presetName: 'somepreset', + presetPath: 'somefile', presetSource: 'github', }); }); @@ -350,8 +350,8 @@ describe('config/presets/index', () => { ).toEqual({ packageName: 'some/repo', params: undefined, - presetName: 'somefile/somepreset/somesubpreset', - presetPath: undefined, + presetName: 'somesubpreset', + presetPath: 'somefile/somepreset', presetSource: 'github', }); }); diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts index 49450e8e8ad3bb..d60baf3122ecb4 100644 --- a/lib/config/presets/index.ts +++ b/lib/config/presets/index.ts @@ -35,6 +35,13 @@ const presetSources: Record = { internal, }; +const nonScopedPresetWithSubdirRegex = regEx( + /^(?~?[\w\-./]+?)\/\/(?:(?[\w\-./]+)\/)?(?[\w\-.]+)(?:#(?[\w\-.]+?))?$/ +); +const gitPresetRegex = regEx( + /^(?[\w\-./]+)(?::(?[\w-./]+\/))?(?::?(?[\w\-.]+))?(?:#(?[\w\-.]+?))?$/ +); + export function replaceArgs( obj: string | string[] | Record | Record[], argMapping: Record @@ -70,6 +77,7 @@ export function parsePreset(input: string): ParsedPreset { let presetPath: string; let packageName: string; let presetName: string; + let packageTag: string; let params: string[]; if (str.startsWith('github>')) { presetSource = 'github'; @@ -138,28 +146,40 @@ export function parsePreset(input: string): ParsedPreset { } } else if (str.includes('//')) { // non-scoped namespace with a subdirectory preset - const re = regEx(/^(~?[\w\-./]+?)\/\/(?:([\w\-./]+)\/)?([\w\-.]+)$/); // Validation if (str.includes(':')) { throw new Error(PRESET_PROHIBITED_SUBPRESET); } - if (!re.test(str)) { + if (!nonScopedPresetWithSubdirRegex.test(str)) { throw new Error(PRESET_INVALID); } - [, packageName, presetPath, presetName] = re.exec(str); + ({ packageName, presetPath, presetName, packageTag } = + nonScopedPresetWithSubdirRegex.exec(str)?.groups || {}); } else { - // non-scoped namespace - [, packageName] = regEx(/(.*?)(:|$)/).exec(str); - presetName = str.slice(packageName.length + 1); + ({ packageName, presetPath, presetName, packageTag } = + gitPresetRegex.exec(str)?.groups || {}); + + if (is.nonEmptyString(presetPath) && presetPath.endsWith('/')) { + presetPath = presetPath.slice(0, -1); + } + if (presetSource === 'npm' && !packageName.startsWith('renovate-config-')) { packageName = `renovate-config-${packageName}`; } - if (presetName === '') { + if (!is.nonEmptyString(presetName)) { presetName = 'default'; } } - return { presetSource, presetPath, packageName, presetName, params }; + + return { + presetSource, + presetPath, + packageName, + presetName, + packageTag, + params, + }; } export async function getPreset( @@ -175,13 +195,20 @@ export async function getPreset( if (newPreset === null) { return {}; } - const { presetSource, packageName, presetPath, presetName, params } = - parsePreset(preset); + const { + presetSource, + packageName, + presetPath, + presetName, + packageTag, + params, + } = parsePreset(preset); let presetConfig = await presetSources[presetSource].getPreset({ packageName, presetPath, presetName, baseConfig, + packageTag, }); if (!presetConfig) { throw new Error(PRESET_DEP_NOT_FOUND); diff --git a/lib/config/presets/local/__snapshots__/index.spec.ts.snap b/lib/config/presets/local/__snapshots__/index.spec.ts.snap index 942564e6df49cd..e40f0b0a9201be 100644 --- a/lib/config/presets/local/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/local/__snapshots__/index.spec.ts.snap @@ -7,6 +7,7 @@ Array [ "default", undefined, undefined, + undefined, ], ] `; @@ -18,6 +19,7 @@ Array [ "default", undefined, undefined, + undefined, ], ] `; @@ -29,6 +31,7 @@ Array [ "default", undefined, "https://git.example.com", + undefined, ], ] `; @@ -40,6 +43,7 @@ Array [ "default", undefined, "https://api.gitea.example.com", + undefined, ], ] `; @@ -51,6 +55,19 @@ Array [ "default", undefined, "https://api.github.example.com", + undefined, + ], +] +`; + +exports[`config/presets/local/index getPreset() forwards to custom github with a tag 1`] = ` +Array [ + Array [ + "some/repo", + "default", + undefined, + "https://api.github.example.com", + "someTag", ], ] `; @@ -62,6 +79,19 @@ Array [ "default", undefined, "https://gitlab.example.com/api/v4", + undefined, + ], +] +`; + +exports[`config/presets/local/index getPreset() forwards to custom gitlab with a tag 1`] = ` +Array [ + Array [ + "some/repo", + "default", + undefined, + "https://gitlab.example.com/api/v4", + "someTag", ], ] `; @@ -73,6 +103,7 @@ Array [ "default", undefined, undefined, + undefined, ], ] `; @@ -84,6 +115,19 @@ Array [ "default", undefined, undefined, + undefined, + ], +] +`; + +exports[`config/presets/local/index getPreset() forwards to github with a tag 1`] = ` +Array [ + Array [ + "some/repo", + "default", + undefined, + undefined, + "someTag", ], ] `; @@ -95,6 +139,19 @@ Array [ "default", undefined, undefined, + undefined, + ], +] +`; + +exports[`config/presets/local/index getPreset() forwards to gitlab with a tag 1`] = ` +Array [ + Array [ + "some/repo", + "default", + undefined, + undefined, + "someTag", ], ] `; diff --git a/lib/config/presets/local/index.spec.ts b/lib/config/presets/local/index.spec.ts index 9728192a782d54..db0f01f942b6a1 100644 --- a/lib/config/presets/local/index.spec.ts +++ b/lib/config/presets/local/index.spec.ts @@ -140,6 +140,30 @@ describe('config/presets/local/index', () => { expect(github.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); expect(content).toEqual({ resolved: 'preset' }); }); + it('forwards to github with a tag', async () => { + const content = await local.getPreset({ + packageName: 'some/repo', + packageTag: 'someTag', + baseConfig: { + platform: 'github', + }, + }); + expect(github.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); + expect(content).toEqual({ resolved: 'preset' }); + }); + it('forwards to custom github with a tag', async () => { + const content = await local.getPreset({ + packageName: 'some/repo', + presetName: 'default', + packageTag: 'someTag', + baseConfig: { + platform: 'github', + endpoint: 'https://api.github.example.com', + }, + }); + expect(github.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); + expect(content).toEqual({ resolved: 'preset' }); + }); it('forwards to gitlab', async () => { const content = await local.getPreset({ @@ -164,5 +188,30 @@ describe('config/presets/local/index', () => { expect(gitlab.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); expect(content).toEqual({ resolved: 'preset' }); }); + it('forwards to gitlab with a tag', async () => { + const content = await local.getPreset({ + packageName: 'some/repo', + presetName: 'default', + packageTag: 'someTag', + baseConfig: { + platform: 'GitLab', + }, + }); + expect(gitlab.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); + expect(content).toEqual({ resolved: 'preset' }); + }); + it('forwards to custom gitlab with a tag', async () => { + const content = await local.getPreset({ + packageName: 'some/repo', + presetName: 'default', + packageTag: 'someTag', + baseConfig: { + platform: 'gitlab', + endpoint: 'https://gitlab.example.com/api/v4', + }, + }); + expect(gitlab.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); + expect(content).toEqual({ resolved: 'preset' }); + }); }); }); diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index 440197771eb268..31fec21ad53933 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -20,6 +20,7 @@ export function getPreset({ packageName: pkgName, presetName = 'default', presetPath, + packageTag, baseConfig, }: PresetConfig): Promise { const { platform, endpoint } = baseConfig; @@ -36,6 +37,7 @@ export function getPreset({ pkgName, presetName, presetPath, - endpoint + endpoint, + packageTag ); } diff --git a/lib/config/presets/types.ts b/lib/config/presets/types.ts index 4833c59b5cdbfc..9c3b6732fba71f 100644 --- a/lib/config/presets/types.ts +++ b/lib/config/presets/types.ts @@ -8,6 +8,7 @@ export type PresetConfig = { presetPath?: string; presetName?: string; baseConfig?: RenovateConfig; + packageTag?: string; }; export interface PresetApi { @@ -19,13 +20,15 @@ export interface ParsedPreset { packageName: string; presetPath?: string; presetName: string; + packageTag?: string; params?: string[]; } export type PresetFetcher = ( repo: string, fileName: string, - endpoint: string + endpoint: string, + packageTag?: string ) => Promise; export type FetchPresetConfig = { @@ -33,5 +36,6 @@ export type FetchPresetConfig = { filePreset: string; presetPath?: string; endpoint: string; + packageTag?: string; fetch: PresetFetcher; }; diff --git a/lib/config/presets/util.ts b/lib/config/presets/util.ts index 1203e4592729bb..8b044b5a33e236 100644 --- a/lib/config/presets/util.ts +++ b/lib/config/presets/util.ts @@ -15,6 +15,7 @@ export async function fetchPreset({ filePreset, presetPath, endpoint, + packageTag = null, fetch, }: FetchPresetConfig): Promise { // eslint-disable-next-line no-param-reassign @@ -28,7 +29,8 @@ export async function fetchPreset({ jsonContent = await fetch( pkgName, buildFilePath('default.json'), - endpoint + endpoint, + packageTag ); } catch (err) { if (err.message !== PRESET_DEP_NOT_FOUND) { @@ -40,14 +42,16 @@ export async function fetchPreset({ jsonContent = await fetch( pkgName, buildFilePath('renovate.json'), - endpoint + endpoint, + packageTag ); } } else { jsonContent = await fetch( pkgName, buildFilePath(`${fileName}.json`), - endpoint + endpoint, + packageTag ); }