From e5c92e4cd9370441bfec1e99a4916ee9d7f0a34f Mon Sep 17 00:00:00 2001 From: Armaan Tobaccowalla <armaan@tobaccowalla.com> Date: Sat, 20 Mar 2021 19:03:10 -0400 Subject: [PATCH] feat(config): Support presets in subdirectories (#8724) --- docs/usage/config-presets.md | 23 ++++---- .../presets/__snapshots__/index.spec.ts.snap | 54 +++++++++++++++++++ .../__snapshots__/index.spec.ts.snap | 17 ++++++ .../presets/bitbucket-server/index.spec.ts | 20 +++++++ lib/config/presets/bitbucket-server/index.ts | 2 + .../gitea/__snapshots__/index.spec.ts.snap | 16 ++++++ lib/config/presets/gitea/index.spec.ts | 19 ++++++- lib/config/presets/gitea/index.ts | 5 +- .../github/__snapshots__/index.spec.ts.snap | 16 ++++++ lib/config/presets/github/index.spec.ts | 19 ++++++- lib/config/presets/github/index.ts | 5 +- .../gitlab/__snapshots__/index.spec.ts.snap | 43 +++++++++++---- lib/config/presets/gitlab/index.spec.ts | 32 ++++++++++- lib/config/presets/gitlab/index.ts | 5 +- lib/config/presets/index.spec.ts | 40 ++++++++++++++ lib/config/presets/index.ts | 29 +++++++++- .../local/__snapshots__/index.spec.ts.snap | 7 +++ lib/config/presets/local/index.ts | 23 ++++++-- lib/config/presets/types.ts | 1 + lib/config/presets/util.ts | 22 ++++++-- 20 files changed, 366 insertions(+), 32 deletions(-) diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md index fe0b5776e7..867aac1a4e 100644 --- a/docs/usage/config-presets.md +++ b/docs/usage/config-presets.md @@ -33,15 +33,20 @@ In order to achieve these goals, preset configs allow for a very modular approac 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` or `renovate.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` or `renovate.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` or `renovate.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` or `renovate.json` | +| name | example use | preset | resolves as | filename | +| ----------------------- | -------------------------- | --------- | ------------------------------------ | --------------------------------- | +| GitHub default | `github>abc/foo` | `default` | `https://github.com/abc/foo` | `default.json` or `renovate.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` or `renovate.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` or `renovate.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` or `renovate.json` | +| Local with preset path | `local>abc/foo//path/xyz` | `default` | `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. ## Example configs diff --git a/lib/config/presets/__snapshots__/index.spec.ts.snap b/lib/config/presets/__snapshots__/index.spec.ts.snap index ae16bf2708..f3ab55e37a 100644 --- a/lib/config/presets/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/__snapshots__/index.spec.ts.snap @@ -87,6 +87,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "gitea", } `; @@ -96,6 +97,17 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "default", + "presetPath": undefined, + "presetSource": "github", +} +`; + +exports[`config/presets parsePreset parses github subdirectories 1`] = ` +Object { + "packageName": "some/repo", + "params": undefined, + "presetName": "somefile", + "presetPath": "somepath/somesubpath", "presetSource": "github", } `; @@ -105,6 +117,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "somefile", + "presetPath": undefined, "presetSource": "github", } `; @@ -114,6 +127,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "somefile/somepreset/somesubpreset", + "presetPath": undefined, "presetSource": "github", } `; @@ -123,6 +137,17 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "somefile/somepreset", + "presetPath": undefined, + "presetSource": "github", +} +`; + +exports[`config/presets parsePreset parses github toplevel file using subdirectory syntax 1`] = ` +Object { + "packageName": "some/repo", + "params": undefined, + "presetName": "somefile", + "presetPath": undefined, "presetSource": "github", } `; @@ -132,6 +157,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "gitlab", } `; @@ -141,6 +167,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "local", } `; @@ -150,6 +177,7 @@ Object { "packageName": "some/repo", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "local", } `; @@ -159,6 +187,7 @@ Object { "packageName": "default", "params": undefined, "presetName": "base", + "presetPath": undefined, "presetSource": "internal", } `; @@ -171,6 +200,7 @@ Object { "eslint", ], "presetName": "group", + "presetPath": undefined, "presetSource": "internal", } `; @@ -180,6 +210,7 @@ Object { "packageName": "renovate-config-somepackage", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "npm", } `; @@ -189,6 +220,7 @@ Object { "packageName": "renovate-config-somepackage", "params": undefined, "presetName": "webapp", + "presetPath": undefined, "presetSource": "npm", } `; @@ -198,6 +230,7 @@ Object { "packageName": "renovate-config-somepackage", "params": undefined, "presetName": "webapp", + "presetPath": undefined, "presetSource": "npm", } `; @@ -209,6 +242,7 @@ Object { "param1", ], "presetName": "webapp", + "presetPath": undefined, "presetSource": "npm", } `; @@ -218,6 +252,7 @@ Object { "packageName": "@somescope/somepackagename", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "npm", } `; @@ -231,6 +266,7 @@ Object { "param3", ], "presetName": "default", + "presetPath": undefined, "presetSource": "npm", } `; @@ -240,6 +276,7 @@ Object { "packageName": "@somescope/somepackagename", "params": undefined, "presetName": "somePresetName", + "presetPath": undefined, "presetSource": "npm", } `; @@ -252,6 +289,7 @@ Object { "param2", ], "presetName": "somePresetName", + "presetPath": undefined, "presetSource": "npm", } `; @@ -261,6 +299,7 @@ Object { "packageName": "@somescope/renovate-config", "params": undefined, "presetName": "somePresetName", + "presetPath": undefined, "presetSource": "npm", } `; @@ -272,6 +311,7 @@ Object { "param1", ], "presetName": "somePresetName", + "presetPath": undefined, "presetSource": "npm", } `; @@ -281,6 +321,7 @@ Object { "packageName": "@somescope/renovate-config", "params": undefined, "presetName": "default", + "presetPath": undefined, "presetSource": "npm", } `; @@ -292,6 +333,7 @@ Object { "param1", ], "presetName": "default", + "presetPath": undefined, "presetSource": "npm", } `; @@ -514,6 +556,18 @@ exports[`config/presets resolvePreset throws if invalid preset file 2`] = `"Cann exports[`config/presets resolvePreset throws if invalid preset file 3`] = `undefined`; +exports[`config/presets resolvePreset throws if path + invalid syntax 1`] = `undefined`; + +exports[`config/presets resolvePreset throws if path + invalid syntax 2`] = `"Preset is invalid (github>user/repo//)"`; + +exports[`config/presets resolvePreset throws if path + invalid syntax 3`] = `undefined`; + +exports[`config/presets resolvePreset throws if path + sub-preset 1`] = `undefined`; + +exports[`config/presets resolvePreset throws if path + sub-preset 2`] = `"Sub-presets cannot be combined with a custom path (github>user/repo//path:subpreset)"`; + +exports[`config/presets resolvePreset throws if path + sub-preset 3`] = `undefined`; + exports[`config/presets resolvePreset throws if valid and invalid 1`] = `undefined`; exports[`config/presets resolvePreset throws if valid and invalid 2`] = `"Preset name not found within published preset config (wrongpreset:invalid-preset)"`; diff --git a/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap b/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap index 6f362876bf..58e8780369 100644 --- a/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/bitbucket-server/__snapshots__/index.spec.ts.snap @@ -90,3 +90,20 @@ Array [ }, ] `; + +exports[`config/presets/bitbucket-server/index getPresetFromEndpoint() uses custom path 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "Bearer abc", + "host": "api.github.example.org", + "user-agent": "https://github.com/renovatebot/renovate", + "x-atlassian-token": "no-check", + }, + "method": "GET", + "url": "https://api.github.example.org/rest/api/1.0/projects/some/repos/repo/browse/path/default.json?limit=20000", + }, +] +`; diff --git a/lib/config/presets/bitbucket-server/index.spec.ts b/lib/config/presets/bitbucket-server/index.spec.ts index 0c709b8426..afa5e7022a 100644 --- a/lib/config/presets/bitbucket-server/index.spec.ts +++ b/lib/config/presets/bitbucket-server/index.spec.ts @@ -112,6 +112,26 @@ describe(getName(__filename), () => { await bitbucketServer.getPresetFromEndpoint( 'some/repo', 'default', + undefined, + 'https://api.github.example.org' + ) + ).toEqual({ from: 'api' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('uses custom path', async () => { + httpMock + .scope('https://api.github.example.org') + .get(`${basePath}/path/default.json`) + .query({ limit: 20000 }) + .reply(200, { + isLastPage: true, + lines: [{ text: '{"from":"api"}' }], + }); + expect( + await bitbucketServer.getPresetFromEndpoint( + 'some/repo', + 'default', + 'path', 'https://api.github.example.org' ) ).toEqual({ from: 'api' }); diff --git a/lib/config/presets/bitbucket-server/index.ts b/lib/config/presets/bitbucket-server/index.ts index c50497c408..727d8246da 100644 --- a/lib/config/presets/bitbucket-server/index.ts +++ b/lib/config/presets/bitbucket-server/index.ts @@ -48,11 +48,13 @@ export async function fetchJSONFile( export function getPresetFromEndpoint( pkgName: string, filePreset: string, + presetPath: string, endpoint: string ): Promise<Preset> { return fetchPreset({ pkgName, filePreset, + presetPath, endpoint, fetch: fetchJSONFile, }); diff --git a/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap index 80dcd962aa..69dd66a352 100644 --- a/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/gitea/__snapshots__/index.spec.ts.snap @@ -22,6 +22,22 @@ Array [ ] `; +exports[`config/presets/gitea/index getPreset() should query custom paths 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "authorization": "token abc", + "host": "gitea.com", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://gitea.com/api/v1/repos/some/repo/contents/path%2Fcustom.json?", + }, +] +`; + exports[`config/presets/gitea/index getPreset() should query preset within the file 1`] = ` Array [ Object { diff --git a/lib/config/presets/gitea/index.spec.ts b/lib/config/presets/gitea/index.spec.ts index d011bf401a..ddab9e13ab 100644 --- a/lib/config/presets/gitea/index.spec.ts +++ b/lib/config/presets/gitea/index.spec.ts @@ -142,6 +142,22 @@ describe(getName(__filename), () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('should query custom paths', async () => { + httpMock + .scope(giteaApiHost) + .get(`${basePath}/path%2Fcustom.json`) + .reply(200, { + content: Buffer.from('{"foo":"bar"}').toString('base64'), + }); + const content = await gitea.getPreset({ + packageName: 'some/repo', + presetName: 'custom', + presetPath: 'path', + }); + expect(content).toEqual({ foo: 'bar' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('should throws not-found', async () => { httpMock .scope(giteaApiHost) @@ -168,7 +184,7 @@ describe(getName(__filename), () => { content: Buffer.from('{"from":"api"}').toString('base64'), }); expect( - await gitea.getPresetFromEndpoint('some/repo', 'default') + await gitea.getPresetFromEndpoint('some/repo', 'default', undefined) ).toEqual({ from: 'api' }); expect(httpMock.getTrace()).toMatchSnapshot(); }); @@ -185,6 +201,7 @@ describe(getName(__filename), () => { .getPresetFromEndpoint( 'some/repo', 'default', + undefined, 'https://api.gitea.example.org' ) .catch(() => ({ from: 'api' })) diff --git a/lib/config/presets/gitea/index.ts b/lib/config/presets/gitea/index.ts index b36eda679f..ae3229b721 100644 --- a/lib/config/presets/gitea/index.ts +++ b/lib/config/presets/gitea/index.ts @@ -40,11 +40,13 @@ export async function fetchJSONFile( export function getPresetFromEndpoint( pkgName: string, filePreset: string, + presetPath: string, endpoint = Endpoint ): Promise<Preset> { return fetchPreset({ pkgName, filePreset, + presetPath, endpoint, fetch: fetchJSONFile, }); @@ -53,6 +55,7 @@ export function getPresetFromEndpoint( export function getPreset({ packageName: pkgName, presetName = 'default', + presetPath, }: PresetConfig): Promise<Preset> { - return getPresetFromEndpoint(pkgName, presetName, Endpoint); + return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); } diff --git a/lib/config/presets/github/__snapshots__/index.spec.ts.snap b/lib/config/presets/github/__snapshots__/index.spec.ts.snap index 778afb4c98..a9c1e0512e 100644 --- a/lib/config/presets/github/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/github/__snapshots__/index.spec.ts.snap @@ -22,6 +22,22 @@ Array [ ] `; +exports[`config/presets/github/index getPreset() should query custom paths 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate", + "authorization": "token abc", + "host": "api.github.com", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://api.github.com/repos/some/repo/contents/path/custom.json", + }, +] +`; + exports[`config/presets/github/index getPreset() should query preset within the file 1`] = ` Array [ Object { diff --git a/lib/config/presets/github/index.spec.ts b/lib/config/presets/github/index.spec.ts index 97b0a5922f..6de3ecc7b9 100644 --- a/lib/config/presets/github/index.spec.ts +++ b/lib/config/presets/github/index.spec.ts @@ -140,6 +140,22 @@ describe(getName(__filename), () => { expect(httpMock.getTrace()).toMatchSnapshot(); }); + it('should query custom paths', async () => { + httpMock + .scope(githubApiHost) + .get(`${basePath}/path/custom.json`) + .reply(200, { + content: Buffer.from('{"foo":"bar"}').toString('base64'), + }); + const content = await github.getPreset({ + packageName: 'some/repo', + presetName: 'custom', + presetPath: 'path', + }); + expect(content).toEqual({ foo: 'bar' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + it('should throws not-found', async () => { httpMock .scope(githubApiHost) @@ -166,7 +182,7 @@ describe(getName(__filename), () => { content: Buffer.from('{"from":"api"}').toString('base64'), }); expect( - await github.getPresetFromEndpoint('some/repo', 'default') + await github.getPresetFromEndpoint('some/repo', 'default', undefined) ).toEqual({ from: 'api' }); expect(httpMock.getTrace()).toMatchSnapshot(); }); @@ -183,6 +199,7 @@ describe(getName(__filename), () => { .getPresetFromEndpoint( 'some/repo', 'default', + undefined, 'https://api.github.example.org' ) .catch(() => ({ from: 'api' })) diff --git a/lib/config/presets/github/index.ts b/lib/config/presets/github/index.ts index 08f7d2eb41..fef6d0f124 100644 --- a/lib/config/presets/github/index.ts +++ b/lib/config/presets/github/index.ts @@ -40,11 +40,13 @@ export async function fetchJSONFile( export function getPresetFromEndpoint( pkgName: string, filePreset: string, + presetPath: string, endpoint = Endpoint ): Promise<Preset> { return fetchPreset({ pkgName, filePreset, + presetPath, endpoint, fetch: fetchJSONFile, }); @@ -53,6 +55,7 @@ export function getPresetFromEndpoint( export function getPreset({ packageName: pkgName, presetName = 'default', + presetPath, }: PresetConfig): Promise<Preset> { - return getPresetFromEndpoint(pkgName, presetName, Endpoint); + return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); } diff --git a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap index b79273bd38..e612a7ae60 100644 --- a/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/gitlab/__snapshots__/index.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`config/presets/gitlab/index getPreset() should return the preset 1`] = ` +exports[`config/presets/gitlab/index getPreset() should query custom paths 1`] = ` Array [ Object { "headers": Object { @@ -20,12 +20,12 @@ Array [ "user-agent": "https://github.com/renovatebot/renovate", }, "method": "GET", - "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master", + "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/path%2Fcustom.json/raw?ref=master", }, ] `; -exports[`config/presets/gitlab/index getPreset() throws if missing 1`] = ` +exports[`config/presets/gitlab/index getPreset() should return the preset 1`] = ` Array [ Object { "headers": Object { @@ -47,6 +47,11 @@ Array [ "method": "GET", "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master", }, +] +`; + +exports[`config/presets/gitlab/index getPreset() throws EXTERNAL_HOST_ERROR 1`] = ` +Array [ Object { "headers": Object { "accept": "application/json", @@ -57,6 +62,11 @@ Array [ "method": "GET", "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches", }, +] +`; + +exports[`config/presets/gitlab/index getPreset() throws if missing 1`] = ` +Array [ Object { "headers": Object { "accept": "application/json", @@ -65,13 +75,18 @@ Array [ "user-agent": "https://github.com/renovatebot/renovate", }, "method": "GET", - "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/renovate.json/raw?ref=master", + "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "host": "gitlab.com", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/default.json/raw?ref=master", }, -] -`; - -exports[`config/presets/gitlab/index getPreset() throws EXTERNAL_HOST_ERROR 1`] = ` -Array [ Object { "headers": Object { "accept": "application/json", @@ -82,6 +97,16 @@ Array [ "method": "GET", "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/branches", }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "host": "gitlab.com", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://gitlab.com/api/v4/projects/some%2Frepo/repository/files/renovate.json/raw?ref=master", + }, ] `; diff --git a/lib/config/presets/gitlab/index.spec.ts b/lib/config/presets/gitlab/index.spec.ts index f2ec07c8b4..c6a4dbe77d 100644 --- a/lib/config/presets/gitlab/index.spec.ts +++ b/lib/config/presets/gitlab/index.spec.ts @@ -65,6 +65,31 @@ describe(getName(__filename), () => { expect(content).toEqual({ foo: 'bar' }); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + it('should query custom paths', async () => { + httpMock + .scope(gitlabApiHost) + .get(`${basePath}/branches`) + .reply(200, [ + { + name: 'devel', + }, + { + name: 'master', + default: true, + }, + ]) + .get(`${basePath}/files/path%2Fcustom.json/raw?ref=master`) + .reply(200, { foo: 'bar' }, {}); + + const content = await gitlab.getPreset({ + packageName: 'some/repo', + presetPath: 'path', + presetName: 'custom', + }); + expect(content).toEqual({ foo: 'bar' }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); }); describe('getPresetFromEndpoint()', () => { @@ -81,7 +106,11 @@ describe(getName(__filename), () => { .get(`${basePath}/files/some.json/raw?ref=devel`) .reply(200, { preset: { file: {} } }); expect( - await gitlab.getPresetFromEndpoint('some/repo', 'some/preset/file') + await gitlab.getPresetFromEndpoint( + 'some/repo', + 'some/preset/file', + undefined + ) ).toEqual({}); expect(httpMock.getTrace()).toMatchSnapshot(); }); @@ -102,6 +131,7 @@ describe(getName(__filename), () => { gitlab.getPresetFromEndpoint( 'some/repo', 'some/preset/file', + undefined, 'https://gitlab.example.org/api/v4' ) ).rejects.toThrow(PRESET_DEP_NOT_FOUND); diff --git a/lib/config/presets/gitlab/index.ts b/lib/config/presets/gitlab/index.ts index 6b0f4b8e8b..829ee1e022 100644 --- a/lib/config/presets/gitlab/index.ts +++ b/lib/config/presets/gitlab/index.ts @@ -57,11 +57,13 @@ export async function fetchJSONFile( export function getPresetFromEndpoint( pkgName: string, presetName: string, + presetPath: string, endpoint = Endpoint ): Promise<Preset> { return fetchPreset({ pkgName, filePreset: presetName, + presetPath, endpoint, fetch: fetchJSONFile, }); @@ -69,7 +71,8 @@ export function getPresetFromEndpoint( export function getPreset({ packageName: pkgName, + presetPath, presetName = 'default', }: PresetConfig): Promise<Preset> { - return getPresetFromEndpoint(pkgName, presetName, Endpoint); + return getPresetFromEndpoint(pkgName, presetName, presetPath, Endpoint); } diff --git a/lib/config/presets/index.spec.ts b/lib/config/presets/index.spec.ts index cad4bf87a8..06ab52a502 100644 --- a/lib/config/presets/index.spec.ts +++ b/lib/config/presets/index.spec.ts @@ -75,6 +75,36 @@ describe('config/presets', () => { expect(e.validationMessage).toMatchSnapshot(); }); + it('throws if path + invalid syntax', async () => { + config.foo = 1; + config.extends = ['github>user/repo//']; + let e: Error; + try { + await presets.resolveConfigPresets(config); + } catch (err) { + e = err; + } + expect(e).toBeDefined(); + expect(e.configFile).toMatchSnapshot(); + expect(e.validationError).toMatchSnapshot(); + expect(e.validationMessage).toMatchSnapshot(); + }); + + it('throws if path + sub-preset', async () => { + config.foo = 1; + config.extends = ['github>user/repo//path:subpreset']; + let e: Error; + try { + await presets.resolveConfigPresets(config); + } catch (err) { + e = err; + } + expect(e).toBeDefined(); + expect(e.configFile).toMatchSnapshot(); + expect(e.validationError).toMatchSnapshot(); + expect(e.validationMessage).toMatchSnapshot(); + }); + it('throws noconfig', async () => { config.foo = 1; config.extends = ['noconfig:base']; @@ -263,6 +293,16 @@ describe('config/presets', () => { ) ).toMatchSnapshot(); }); + it('parses github subdirectories', () => { + expect( + presets.parsePreset('github>some/repo//somepath/somesubpath/somefile') + ).toMatchSnapshot(); + }); + it('parses github toplevel file using subdirectory syntax', () => { + expect( + presets.parsePreset('github>some/repo//somefile') + ).toMatchSnapshot(); + }); it('parses gitlab', () => { expect(presets.parsePreset('gitlab>some/repo')).toMatchSnapshot(); }); diff --git a/lib/config/presets/index.ts b/lib/config/presets/index.ts index 44836d5290..b4dbce3c55 100644 --- a/lib/config/presets/index.ts +++ b/lib/config/presets/index.ts @@ -60,6 +60,7 @@ export function replaceArgs( export function parsePreset(input: string): ParsedPreset { let str = input; let presetSource: string; + let presetPath: string; let packageName: string; let presetName: string; let params: string[]; @@ -127,6 +128,18 @@ export function parsePreset(input: string): ParsedPreset { } else { presetName = str.slice(1); } + } else if (str.includes('//')) { + // non-scoped namespace with a subdirectory preset + const re = /^([\w./]+?)\/\/(?:([\w./]+)\/)?([\w.]+)$/; + + // Validation + if (str.includes(':')) { + throw new Error('prohibited sub-preset'); + } + if (!re.test(str)) { + throw new Error('invalid preset'); + } + [, packageName, presetPath, presetName] = re.exec(str); } else { // non-scoped namespace [, packageName] = /(.*?)(:|$)/.exec(str); @@ -138,7 +151,7 @@ export function parsePreset(input: string): ParsedPreset { presetName = 'default'; } } - return { presetSource, packageName, presetName, params }; + return { presetSource, presetPath, packageName, presetName, params }; } export async function getPreset( @@ -146,9 +159,16 @@ export async function getPreset( baseConfig?: RenovateConfig ): Promise<RenovateConfig> { logger.trace(`getPreset(${preset})`); - const { presetSource, packageName, presetName, params } = parsePreset(preset); + const { + presetSource, + packageName, + presetPath, + presetName, + params, + } = parsePreset(preset); let presetConfig = await presetSources[presetSource].getPreset({ packageName, + presetPath, presetName, baseConfig, }); @@ -237,6 +257,10 @@ export async function resolveConfigPresets( error.validationError = `Preset package is missing a renovate-config entry (${preset})`; } else if (err.message === 'preset not found') { error.validationError = `Preset name not found within published preset config (${preset})`; + } else if (err.message === 'invalid preset') { + error.validationError = `Preset is invalid (${preset})`; + } else if (err.message === 'prohibited sub-preset') { + error.validationError = `Sub-presets cannot be combined with a custom path (${preset})`; } // istanbul ignore if if (existingPresets.length) { @@ -307,6 +331,7 @@ export async function resolveConfigPresets( export interface ParsedPreset { presetSource: string; packageName: string; + presetPath?: string; presetName: string; params?: string[]; } diff --git a/lib/config/presets/local/__snapshots__/index.spec.ts.snap b/lib/config/presets/local/__snapshots__/index.spec.ts.snap index c0887b2337..c580f9c3ff 100644 --- a/lib/config/presets/local/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/local/__snapshots__/index.spec.ts.snap @@ -5,6 +5,7 @@ Array [ Array [ "some/repo", "default", + undefined, "https://git.example.com", ], ] @@ -17,6 +18,7 @@ Array [ Array [ "some/repo", "default", + undefined, "https://api.gitea.example.com", ], ] @@ -29,6 +31,7 @@ Array [ Array [ "some/repo", "default", + undefined, "https://api.github.example.com", ], ] @@ -45,6 +48,7 @@ Array [ Array [ "some/repo", "default", + undefined, "https://gitlab.example.com/api/v4", ], ] @@ -62,6 +66,7 @@ Array [ "some/repo", "default", undefined, + undefined, ], ] `; @@ -74,6 +79,7 @@ Array [ "some/repo", "default", undefined, + undefined, ], ] `; @@ -90,6 +96,7 @@ Array [ "some/repo", "default", undefined, + undefined, ], ] `; diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index 63e4400bc9..4a06d9f227 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -13,6 +13,7 @@ import type { Preset, PresetConfig } from '../types'; export function getPreset({ packageName: pkgName, presetName = 'default', + presetPath, baseConfig, }: PresetConfig): Promise<Preset> { const { platform, endpoint } = baseConfig; @@ -21,17 +22,33 @@ export function getPreset({ } switch (platform.toLowerCase()) { case PLATFORM_TYPE_GITLAB: - return gitlab.getPresetFromEndpoint(pkgName, presetName, endpoint); + return gitlab.getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + endpoint + ); case PLATFORM_TYPE_GITHUB: - return github.getPresetFromEndpoint(pkgName, presetName, endpoint); + return github.getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + endpoint + ); case PLATFORM_TYPE_BITBUCKET_SERVER: return bitbucketServer.getPresetFromEndpoint( pkgName, presetName, + presetPath, endpoint ); case PLATFORM_TYPE_GITEA: - return gitea.getPresetFromEndpoint(pkgName, presetName, endpoint); + return gitea.getPresetFromEndpoint( + pkgName, + presetName, + presetPath, + endpoint + ); default: throw new Error( `Unsupported platform '${baseConfig.platform}' for local preset.` diff --git a/lib/config/presets/types.ts b/lib/config/presets/types.ts index 6aac5a5c18..7296dda27a 100644 --- a/lib/config/presets/types.ts +++ b/lib/config/presets/types.ts @@ -5,6 +5,7 @@ export type Preset = RenovateConfig & Record<string, unknown>; export type PresetConfig = { packageName: string; + presetPath?: string; presetName?: string; baseConfig?: RenovateConfig; }; diff --git a/lib/config/presets/util.ts b/lib/config/presets/util.ts index 500f439f12..12c0e88576 100644 --- a/lib/config/presets/util.ts +++ b/lib/config/presets/util.ts @@ -14,6 +14,7 @@ export type PresetFetcher = ( export type FetchPresetConfig = { pkgName: string; filePreset: string; + presetPath?: string; endpoint: string; fetch: PresetFetcher; }; @@ -21,25 +22,40 @@ export type FetchPresetConfig = { export async function fetchPreset({ pkgName, filePreset, + presetPath, endpoint, fetch, }: FetchPresetConfig): Promise<Preset | undefined> { // eslint-disable-next-line no-param-reassign endpoint = ensureTrailingSlash(endpoint); const [fileName, presetName, subPresetName] = filePreset.split('/'); + const pathPrefix = presetPath ? `${presetPath}/` : ''; + const buildFilePath = (name: string): string => `${pathPrefix}${name}`; let jsonContent: any | undefined; if (fileName === 'default') { try { - jsonContent = await fetch(pkgName, 'default.json', endpoint); + jsonContent = await fetch( + pkgName, + buildFilePath('default.json'), + endpoint + ); } catch (err) { if (err.message !== PRESET_DEP_NOT_FOUND) { throw err; } logger.debug('default.json preset not found - trying renovate.json'); - jsonContent = await fetch(pkgName, 'renovate.json', endpoint); + jsonContent = await fetch( + pkgName, + buildFilePath('renovate.json'), + endpoint + ); } } else { - jsonContent = await fetch(pkgName, `${fileName}.json`, endpoint); + jsonContent = await fetch( + pkgName, + buildFilePath(`${fileName}.json`), + endpoint + ); } if (!jsonContent) { -- GitLab