diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md index 867aac1a4e067dc6fc9e2672c0f22237d519db28..abb41cfbe19520e1e2a98318cb217b7da54996f1 100644 --- a/docs/usage/config-presets.md +++ b/docs/usage/config-presets.md @@ -202,7 +202,7 @@ To host your preset config on Gitea: Renovate also supports local presets, e.g. presets that are hosted on the same platform as the target repository. This is especially helpful in self-hosted scenarios where public presets cannot be used. -Local presets are only supported on GitHub, GitLab, Gitea and Bitbucket Server. +Local presets are only supported on GitHub, GitLab, Gitea, Bitbucket Cloud and Bitbucket Server. Local presets are specified either by leaving out any prefix, e.g. `owner/name`, or explicitly by adding a `local>` prefix, e.g. `local>owner/name`. Renovate will determine the current platform and look up the preset from there. diff --git a/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap b/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..ec37a0babb73f154f9b1b7e00601d4f6a2d9cf6d --- /dev/null +++ b/lib/config/presets/bitbucket/__snapshots__/index.spec.ts.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config/presets/bitbucket/index fetchJSONFile() returns JSON 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate", + "host": "api.bitbucket.org", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json", + }, +] +`; + +exports[`config/presets/bitbucket/index fetchJSONFile() throws on error 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate", + "host": "api.bitbucket.org", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json", + }, +] +`; + +exports[`config/presets/bitbucket/index fetchJSONFile() throws on invalid json 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate", + "host": "api.bitbucket.org", + "user-agent": "https://github.com/renovatebot/renovate", + }, + "method": "GET", + "url": "https://api.bitbucket.org/2.0/repositories/some/repo/src/HEAD/some-filename.json", + }, +] +`; diff --git a/lib/config/presets/bitbucket/index.spec.ts b/lib/config/presets/bitbucket/index.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1f97b45d82fc7ca4ab76c362791d1b43c2e2140 --- /dev/null +++ b/lib/config/presets/bitbucket/index.spec.ts @@ -0,0 +1,77 @@ +import * as httpMock from '../../../../test/http-mock'; +import { getName } from '../../../../test/util'; +import { setPlatformApi } from '../../../platform'; +import { PRESET_DEP_NOT_FOUND, PRESET_INVALID_JSON } from '../util'; +import * as bitbucket from '.'; + +jest.unmock('../../../platform'); + +const baseUrl = 'https://api.bitbucket.org'; +const basePath = '/2.0/repositories/some/repo/src/HEAD'; + +describe(getName(__filename), () => { + beforeAll(() => { + setPlatformApi('bitbucket'); + }); + + beforeEach(() => { + httpMock.setup(); + }); + + afterEach(() => { + httpMock.reset(); + }); + + describe('fetchJSONFile()', () => { + it('returns JSON', async () => { + const data = { foo: 'bar' }; + httpMock + .scope(baseUrl) + .get(`${basePath}/some-filename.json`) + .reply(200, JSON.stringify(data)); + + const res = await bitbucket.fetchJSONFile( + 'some/repo', + 'some-filename.json' + ); + expect(res).toEqual(data); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('throws on error', async () => { + httpMock.scope(baseUrl).get(`${basePath}/some-filename.json`).reply(404); + await expect( + bitbucket.fetchJSONFile('some/repo', 'some-filename.json') + ).rejects.toThrow(PRESET_DEP_NOT_FOUND); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('throws on invalid json', async () => { + httpMock + .scope(baseUrl) + .get(`${basePath}/some-filename.json`) + .reply(200, '!@#'); + await expect( + bitbucket.fetchJSONFile('some/repo', 'some-filename.json') + ).rejects.toThrow(PRESET_INVALID_JSON); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + }); + + describe('getPresetFromEndpoint()', () => { + it('uses custom path', async () => { + const data = { foo: 'bar' }; + httpMock + .scope(baseUrl) + .get(`${basePath}/foo/bar/some-filename.json`) + .reply(200, JSON.stringify(data)); + const res = await bitbucket.getPresetFromEndpoint( + 'some/repo', + 'some-filename', + 'foo/bar', + baseUrl + ); + expect(res).toEqual(data); + }); + }); +}); diff --git a/lib/config/presets/bitbucket/index.ts b/lib/config/presets/bitbucket/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..683c271510a3ad08fa6fedb453129e14d1306a22 --- /dev/null +++ b/lib/config/presets/bitbucket/index.ts @@ -0,0 +1 @@ +export { fetchJSONFile, getPresetFromEndpoint } from '../local/common'; diff --git a/lib/config/presets/local/__snapshots__/index.spec.ts.snap b/lib/config/presets/local/__snapshots__/index.spec.ts.snap index 09f1a6658ba302fc99a1b8e69d2a3a8210356539..83a107844f3de628c6577875f71979839f3f1d62 100644 --- a/lib/config/presets/local/__snapshots__/index.spec.ts.snap +++ b/lib/config/presets/local/__snapshots__/index.spec.ts.snap @@ -1,5 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`config/presets/local/index getPreset() forwards to bitbucket 1`] = ` +Array [ + Array [ + "some/repo", + "default", + undefined, + undefined, + ], +] +`; + +exports[`config/presets/local/index getPreset() forwards to bitbucket 2`] = ` +Object { + "resolved": "preset", +} +`; + exports[`config/presets/local/index getPreset() forwards to custom bitbucket-server 1`] = ` Array [ Array [ diff --git a/lib/config/presets/local/common.ts b/lib/config/presets/local/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..457ccb3851ecd31bff250a017a6c23e8efd3692c --- /dev/null +++ b/lib/config/presets/local/common.ts @@ -0,0 +1,53 @@ +import { logger } from '../../../logger'; +import { platform } from '../../../platform'; +import { ExternalHostError } from '../../../types/errors/external-host-error'; +import type { Preset } from '../types'; +import { + PRESET_DEP_NOT_FOUND, + PRESET_INVALID_JSON, + fetchPreset, +} from '../util'; + +export async function fetchJSONFile( + repo: string, + fileName: string, + _endpoint: string = null +): Promise<Preset> { + let raw: string; + try { + raw = await platform.getRawFile(fileName, repo); + } catch (err) { + // istanbul ignore if: not testable with nock + if (err instanceof ExternalHostError) { + throw err; + } + + logger.debug( + { err, repo, fileName }, + `Failed to retrieve ${fileName} from repo ${repo}` + ); + + throw new Error(PRESET_DEP_NOT_FOUND); + } + + try { + return JSON.parse(raw); + } catch (err) { + throw new Error(PRESET_INVALID_JSON); + } +} + +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/local/index.spec.ts b/lib/config/presets/local/index.spec.ts index 6bc484566c2ebcbaa2fdd73f4b47d1a23f400aec..0d72cea5e996c564079871e4fa97d4d6a2e81fd1 100644 --- a/lib/config/presets/local/index.spec.ts +++ b/lib/config/presets/local/index.spec.ts @@ -1,15 +1,18 @@ import { getName, mocked } from '../../../../test/util'; +import * as _bitbucket from '../bitbucket'; import * as _bitbucketServer from '../bitbucket-server'; import * as _gitea from '../gitea'; import * as _github from '../github'; import * as _gitlab from '../gitlab'; import * as local from '.'; +jest.mock('../bitbucket'); jest.mock('../bitbucket-server'); jest.mock('../gitea'); jest.mock('../github'); jest.mock('../gitlab'); +const bitbucket = mocked(_bitbucket); const bitbucketServer = mocked(_bitbucketServer); const gitea = mocked(_gitea); const github = mocked(_github); @@ -19,6 +22,7 @@ describe(getName(__filename), () => { beforeEach(() => { jest.resetAllMocks(); const preset = { resolved: 'preset' }; + bitbucket.getPresetFromEndpoint.mockResolvedValueOnce(preset); bitbucketServer.getPresetFromEndpoint.mockResolvedValueOnce(preset); gitea.getPresetFromEndpoint.mockResolvedValueOnce(preset); github.getPresetFromEndpoint.mockResolvedValueOnce(preset); @@ -48,6 +52,18 @@ describe(getName(__filename), () => { }).rejects.toThrow(); }); + it('forwards to bitbucket', async () => { + const content = await local.getPreset({ + packageName: 'some/repo', + presetName: 'default', + baseConfig: { + platform: 'bitbucket', + }, + }); + expect(bitbucket.getPresetFromEndpoint.mock.calls).toMatchSnapshot(); + expect(content).toMatchSnapshot(); + }); + it('forwards to custom bitbucket-server', async () => { const content = await local.getPreset({ packageName: 'some/repo', diff --git a/lib/config/presets/local/index.ts b/lib/config/presets/local/index.ts index c53ae3d1b98ccef251e253ca30c4530a34587288..87f5822f2bfcc9b6a624a8a945c21f377cd20b81 100644 --- a/lib/config/presets/local/index.ts +++ b/lib/config/presets/local/index.ts @@ -1,9 +1,11 @@ import { + PLATFORM_TYPE_BITBUCKET, PLATFORM_TYPE_BITBUCKET_SERVER, PLATFORM_TYPE_GITEA, PLATFORM_TYPE_GITHUB, PLATFORM_TYPE_GITLAB, } from '../../../constants/platforms'; +import * as bitbucket from '../bitbucket'; import * as bitbucketServer from '../bitbucket-server'; import * as gitea from '../gitea'; import * as github from '../github'; @@ -11,6 +13,7 @@ import * as gitlab from '../gitlab'; import type { Preset, PresetConfig } from '../types'; const resolvers = { + [PLATFORM_TYPE_BITBUCKET]: bitbucket, [PLATFORM_TYPE_BITBUCKET_SERVER]: bitbucketServer, [PLATFORM_TYPE_GITEA]: gitea, [PLATFORM_TYPE_GITHUB]: github,