diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index fdb5e224f9a5083b114560083b09a9050a3018dd..f43a047badbc3b36db9d352c87783539b07a2f6a 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -726,6 +726,10 @@ Also, approval rules overriding should not be [prevented in GitLab settings](htt Configuration added here applies for all Go-related updates, however currently the only supported package manager for Go is the native Go Modules (the `gomod` manager). +For self-hosted users, `GOPROXY`, `GONOPROXY` and `GOPRIVATE` environment variables are supported ([reference](https://golang.org/ref/mod#module-proxy)). + +But when you use the `direct` or `off` keywords Renovate will fallback to its own fetching strategy (i.e. directly from GitHub, etc). + ## group Caution: Advanced functionality only. Do not use unless you know what you're doing. diff --git a/lib/datasource/go/__fixtures__/go-kit.list.txt b/lib/datasource/go/__fixtures__/go-kit.list.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a6a741aa3fac61e0628d173290dd1cfc638b0b9 --- /dev/null +++ b/lib/datasource/go/__fixtures__/go-kit.list.txt @@ -0,0 +1,10 @@ +v0.7.0 +v0.3.0 +v0.8.0 +v0.6.0 +v0.10.0 +v0.5.0 +v0.9.0 +v0.4.0 +v0.1.0 +v0.2.0 diff --git a/lib/datasource/go/__snapshots__/goproxy.spec.ts.snap b/lib/datasource/go/__snapshots__/goproxy.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..dd44219acd48748a728cee064111fb4bcde5096b --- /dev/null +++ b/lib/datasource/go/__snapshots__/goproxy.spec.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`datasource/go/goproxy requests listVersions 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/go-kit/kit/@v/list", + }, +] +`; + +exports[`datasource/go/goproxy requests versionInfo 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/go-kit/kit/@v/v0.5.0.info", + }, +] +`; diff --git a/lib/datasource/go/__snapshots__/index.spec.ts.snap b/lib/datasource/go/__snapshots__/index.spec.ts.snap index 6ea4039550c82ef849ba2d208680e12e1eed6e9f..627971757feec3f41d07105125ee1138890b882f 100644 --- a/lib/datasource/go/__snapshots__/index.spec.ts.snap +++ b/lib/datasource/go/__snapshots__/index.spec.ts.snap @@ -93,6 +93,246 @@ Array [ ] `; +exports[`datasource/go/index getReleases GOPROXY fetches release data from goproxy 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.0.info", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.1.info", + }, +] +`; + +exports[`datasource/go/index getReleases GOPROXY handles comma fallback 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "foo.example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://foo.example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "bar.example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://bar.example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.0.info", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.1.info", + }, +] +`; + +exports[`datasource/go/index getReleases GOPROXY handles pipe fallback 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.0.info", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.1.info", + }, +] +`; + +exports[`datasource/go/index getReleases GOPROXY handles timestamp fetch errors 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.0.info", + }, + Object { + "headers": Object { + "accept": "application/json", + "accept-encoding": "gzip, deflate, br", + "host": "proxy.golang.org", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://proxy.golang.org/github.com/google/btree/@v/v1.0.1.info", + }, +] +`; + +exports[`datasource/go/index getReleases GOPROXY short-circuits with comma fallback 1`] = ` +Array [ + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "foo.example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://foo.example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "bar.example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://bar.example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept-encoding": "gzip, deflate, br", + "host": "baz.example.com", + "user-agent": "RenovateBot/0.0.0-semantic-release (https://github.com/renovatebot/renovate)", + }, + "method": "GET", + "url": "https://baz.example.com/github.com/google/btree/@v/list", + }, + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "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/google/btree/tags?per_page=100", + }, + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "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/google/btree/releases?per_page=100", + }, +] +`; + +exports[`datasource/go/index getReleases GOPROXY skips GONOPROXY and GOPRIVATE packages 1`] = ` +Array [ + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "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/google/btree/tags?per_page=100", + }, + Object { + "headers": Object { + "accept": "application/vnd.github.v3+json", + "accept-encoding": "gzip, deflate, br", + "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/google/btree/releases?per_page=100", + }, +] +`; + exports[`datasource/go/index getReleases handles fyne.io 1`] = ` Object { "releases": Array [ diff --git a/lib/datasource/go/common.ts b/lib/datasource/go/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..32702cb6df713a9ccb59aa49297ceafa367f6bde --- /dev/null +++ b/lib/datasource/go/common.ts @@ -0,0 +1,10 @@ +import { Http } from '../../util/http'; + +export const id = 'go'; + +export const http = new Http(id); + +export enum GoproxyFallback { + WhenNotFoundOrGone = ',', + Always = '|', +} diff --git a/lib/datasource/go/goproxy.spec.ts b/lib/datasource/go/goproxy.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..309579f36c2e46d38c77d64da8dcecd1871fdf1f --- /dev/null +++ b/lib/datasource/go/goproxy.spec.ts @@ -0,0 +1,127 @@ +import * as httpMock from '../../../test/http-mock'; +import { getName, loadFixture } from '../../../test/util'; +import * as memCache from '../../util/cache/memory'; +import { + encodeCase, + listVersions, + parseGoproxy, + parseNoproxy, + versionInfo, +} from './goproxy'; + +describe(getName(), () => { + beforeEach(() => { + memCache.init(); + }); + + afterEach(() => { + memCache.reset(); + }); + + it('encodeCase', () => { + expect(encodeCase('foo')).toBe('foo'); + expect(encodeCase('Foo')).toBe('!foo'); + expect(encodeCase('FOO')).toBe('!f!o!o'); + }); + + describe('requests', () => { + const baseUrl = 'https://proxy.golang.org'; + const lookupName = 'github.com/go-kit/kit'; + + it('listVersions', async () => { + httpMock + .scope(baseUrl) + .get('/github.com/go-kit/kit/@v/list') + .reply(200, loadFixture('go-kit.list.txt')); + + const versions = await listVersions(baseUrl, lookupName); + + expect(versions).not.toBeEmpty(); + expect(versions).toHaveLength(10); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + + it('versionInfo', async () => { + httpMock + .scope(baseUrl) + .get('/github.com/go-kit/kit/@v/v0.5.0.info') + .reply(200, { Version: 'v0.5.0', Time: '2017-06-08T17:28:36Z' }); + + const release = await versionInfo(baseUrl, lookupName, 'v0.5.0'); + + expect(release).toEqual({ + version: 'v0.5.0', + releaseTimestamp: '2017-06-08T17:28:36Z', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + }); + }); + + describe('parseGoproxy', () => { + it('parses single url', () => { + const result = parseGoproxy('foo'); + expect(result).toMatchObject([{ url: 'foo' }]); + }); + + it('parses multiple urls', () => { + const result = parseGoproxy('foo,bar|baz,qux'); + expect(result).toMatchObject([ + { url: 'foo', fallback: ',' }, + { url: 'bar', fallback: '|' }, + { url: 'baz', fallback: ',' }, + { url: 'qux' }, + ]); + }); + + it('ignores everything starting from "direct" and "off" keywords', () => { + expect(parseGoproxy(undefined)).toBeEmpty(); + expect(parseGoproxy(null)).toBeEmpty(); + expect(parseGoproxy('')).toBeEmpty(); + expect(parseGoproxy('off')).toBeEmpty(); + expect(parseGoproxy('direct')).toBeEmpty(); + expect(parseGoproxy('foo,off|direct,qux')).toMatchObject([ + { url: 'foo', fallback: ',' }, + ]); + }); + }); + + describe('parseNoproxy', () => { + it('produces regex', () => { + expect(parseNoproxy(undefined)).toBeNull(); + expect(parseNoproxy(null)).toBeNull(); + expect(parseNoproxy('')).toBeNull(); + expect(parseNoproxy('*')?.source).toEqual('^(?:[^\\/]*)$'); + expect(parseNoproxy('?')?.source).toEqual('^(?:[^\\/])$'); + expect(parseNoproxy('foo')?.source).toEqual('^(?:foo)$'); + expect(parseNoproxy('\\f\\o\\o')?.source).toEqual('^(?:foo)$'); + expect(parseNoproxy('foo,bar')?.source).toEqual('^(?:foo|bar)$'); + expect(parseNoproxy('[abc]')?.source).toEqual('^(?:[abc])$'); + expect(parseNoproxy('[a-c]')?.source).toEqual('^(?:[a-c])$'); + expect(parseNoproxy('[\\a-\\c]')?.source).toEqual('^(?:[a-c])$'); + }); + + it('matches on real package prefixes', () => { + expect(parseNoproxy('ex.co/foo/bar').test('ex.co/foo/bar')).toBeTrue(); + expect(parseNoproxy('*/foo/*').test('example.com/foo/bar')).toBeTrue(); + expect(parseNoproxy('ex.co/foo/*').test('ex.co/foo/bar')).toBeTrue(); + expect(parseNoproxy('ex.co/foo/*').test('ex.co/foo/baz')).toBeTrue(); + expect( + parseNoproxy('ex.co/foo/bar,ex.co/foo/baz').test('ex.co/foo/bar') + ).toBeTrue(); + expect( + parseNoproxy('ex.co/foo/bar,ex.co/foo/baz').test('ex.co/foo/baz') + ).toBeTrue(); + expect( + parseNoproxy('ex.co/foo/bar,ex.co/foo/baz').test('ex.co/foo/qux') + ).toBeFalse(); + }); + + it('Matches from start to end', () => { + expect(parseNoproxy('x').test('x/aba')).toBeFalse(); + expect(parseNoproxy('aba').test('x/aba')).toBeFalse(); + expect(parseNoproxy('x/b').test('x/aba')).toBeFalse(); + expect(parseNoproxy('x/ab').test('x/aba')).toBeFalse(); + expect(parseNoproxy('x/ab[a-b]').test('x/aba')).toBeTrue(); + }); + }); +}); diff --git a/lib/datasource/go/goproxy.ts b/lib/datasource/go/goproxy.ts new file mode 100644 index 0000000000000000000000000000000000000000..84334b919ddebddf4cacfdb00d18bc9030069364 --- /dev/null +++ b/lib/datasource/go/goproxy.ts @@ -0,0 +1,203 @@ +import is from '@sindresorhus/is'; +import moo from 'moo'; +import pAll from 'p-all'; +import { logger } from '../../logger'; +import { regEx } from '../../util/regex'; +import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; +import { GoproxyFallback, http } from './common'; +import type { GoproxyItem, VersionInfo } from './types'; + +const parsedGoproxy: Record<string, GoproxyItem[]> = {}; + +/** + * Parse `GOPROXY` to the sequence of url + fallback strategy tags. + * + * @example + * parseGoproxy('foo.example.com|bar.example.com,baz.example.com') + * // [ + * // { url: 'foo.example.com', fallback: '|' }, + * // { url: 'bar.example.com', fallback: ',' }, + * // { url: 'baz.example.com', fallback: '|' }, + * // ] + * + * @see https://golang.org/ref/mod#goproxy-protocol + */ +export function parseGoproxy( + input: string = process.env.GOPROXY +): GoproxyItem[] { + if (!is.string(input)) { + return []; + } + + if (parsedGoproxy[input]) { + return parsedGoproxy[input]; + } + + let result: GoproxyItem[] = input + .split(/([^,|]*(?:,|\|))/) + .filter(Boolean) + .map((s) => s.split(/(?=,|\|)/)) + .map(([url, separator]) => ({ + url, + fallback: + separator === ',' + ? GoproxyFallback.WhenNotFoundOrGone + : GoproxyFallback.Always, + })); + + // Ignore hosts after any keyword + for (let idx = 0; idx < result.length; idx += 1) { + const { url } = result[idx]; + if (['off', 'direct'].includes(url)) { + result = result.slice(0, idx); + break; + } + } + + parsedGoproxy[input] = result; + return result; +} + +// https://golang.org/pkg/path/#Match +const lexer = moo.states({ + main: { + separator: { + match: /\s*?,\s*?/, + value: (_: string) => '|', + }, + asterisk: { + match: '*', + value: (_: string) => '[^\\/]*', + }, + qmark: { + match: '?', + value: (_: string) => '[^\\/]', + }, + characterRangeOpen: { + match: '[', + push: 'characterRange', + value: (_: string) => '[', + }, + char: /[^*?\\[\n]/, + escapedChar: { + match: /\\./, + value: (s: string) => s.slice(1), + }, + }, + characterRange: { + char: /[^\\\]\n]/, + escapedChar: { + match: /\\./, + value: (s: string) => s.slice(1), + }, + characterRangeEnd: { + match: ']', + pop: 1, + }, + }, +}); + +const parsedNoproxy: Record<string, RegExp | null> = {}; + +export function parseNoproxy( + input: unknown = process.env.GONOPROXY || process.env.GOPRIVATE +): RegExp | null { + if (!is.string(input)) { + return null; + } + if (parsedNoproxy[input] !== undefined) { + return parsedNoproxy[input]; + } + lexer.reset(input); + const noproxyPattern = [...lexer].map(({ value }) => value).join(''); + const result = noproxyPattern ? regEx(`^(?:${noproxyPattern})$`) : null; + parsedNoproxy[input] = result; + return result; +} + +/** + * Avoid ambiguity when serving from case-insensitive file systems. + * + * @see https://golang.org/ref/mod#goproxy-protocol + */ +export function encodeCase(input: string): string { + return input.replace(/([A-Z])/g, (x) => `!${x.toLowerCase()}`); +} + +export async function listVersions( + baseUrl: string, + lookupName: string +): Promise<string[]> { + const url = `${baseUrl}/${encodeCase(lookupName)}/@v/list`; + const { body } = await http.get(url); + return body + .split(/\s+/) + .filter(Boolean) + .filter((x) => x.indexOf('+') === -1); +} + +export async function versionInfo( + baseUrl: string, + lookupName: string, + version: string +): Promise<Release> { + const url = `${baseUrl}/${encodeCase(lookupName)}/@v/${version}.info`; + const res = await http.getJson<VersionInfo>(url); + + const result: Release = { + version: res.body.Version, + }; + + if (res.body.Time) { + result.releaseTimestamp = res.body.Time; + } + + return result; +} + +export async function getReleases( + config: GetReleasesConfig +): Promise<ReleaseResult | null> { + const { lookupName } = config; + + const noproxy = parseNoproxy(); + if (noproxy?.test(lookupName)) { + logger.debug(`Skipping ${lookupName} via GONOPROXY match`); + return null; + } + + const proxyList = parseGoproxy(); + + for (const { url, fallback } of proxyList) { + try { + const versions = await listVersions(url, lookupName); + const queue = versions.map((version) => async (): Promise<Release> => { + try { + return await versionInfo(url, lookupName, version); + } catch (err) { + logger.trace({ err }, `Can't obtain data from ${url}`); + return { version }; + } + }); + const releases = await pAll(queue, { concurrency: 5 }); + if (releases.length) { + return { releases }; + } + } catch (err) { + const statusCode = err?.response?.statusCode; + const canFallback = + fallback === GoproxyFallback.Always + ? true + : statusCode === 404 || statusCode === 410; + const msg = canFallback + ? 'Goproxy error: trying next URL provided with GOPROXY' + : 'Goproxy error: skipping other URLs provided with GOPROXY'; + logger.debug({ err }, msg); + if (!canFallback) { + break; + } + } + } + + return null; +} diff --git a/lib/datasource/go/index.spec.ts b/lib/datasource/go/index.spec.ts index d5c4ea059a00a6b21c354183648418989b570838..9cbf976e3764cb57835208985b4fd59e7090207e 100644 --- a/lib/datasource/go/index.spec.ts +++ b/lib/datasource/go/index.spec.ts @@ -460,5 +460,195 @@ describe(getName(), () => { expect(res).toBeDefined(); expect(httpMock.getTrace()).toMatchSnapshot(); }); + + describe('GOPROXY', () => { + const baseUrl = 'https://proxy.golang.org'; + + afterEach(() => { + delete process.env.GOPROXY; + delete process.env.GONOPROXY; + delete process.env.GOPRIVATE; + }); + + it('skips GONOPROXY and GOPRIVATE packages', async () => { + process.env.GOPROXY = baseUrl; + process.env.GOPRIVATE = 'github.com/google/*'; + + httpMock + .scope('https://api.github.com/') + .get('/repos/google/btree/tags?per_page=100') + .reply(200, [{ name: 'v1.0.0' }, { name: 'v1.0.1' }]) + .get('/repos/google/btree/releases?per_page=100') + .reply(200, []); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res).toEqual({ + releases: [ + { gitRef: 'v1.0.0', version: 'v1.0.0' }, + { gitRef: 'v1.0.1', version: 'v1.0.1' }, + ], + sourceUrl: 'https://github.com/google/btree', + }); + }); + + it('fetches release data from goproxy', async () => { + process.env.GOPROXY = baseUrl; + + httpMock + .scope(`${baseUrl}/github.com/google/btree`) + .get('/@v/list') + .reply(200, 'v1.0.0\nv1.0.1\n') + .get('/@v/v1.0.0.info') + .reply(200, { Version: 'v1.0.0', Time: '2018-08-13T15:31:12Z' }) + .get('/@v/v1.0.1.info') + .reply(200, { Version: 'v1.0.1', Time: '2019-10-16T16:15:28Z' }); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res?.releases).toMatchObject([ + { releaseTimestamp: '2018-08-13T15:31:12.000Z', version: 'v1.0.0' }, + { releaseTimestamp: '2019-10-16T16:15:28.000Z', version: 'v1.0.1' }, + ]); + }); + + it('handles timestamp fetch errors', async () => { + process.env.GOPROXY = baseUrl; + + httpMock + .scope(`${baseUrl}/github.com/google/btree`) + .get('/@v/list') + .reply(200, 'v1.0.0\nv1.0.1\n') + .get('/@v/v1.0.0.info') + .replyWithError('unknown') + .get('/@v/v1.0.1.info') + .reply(410); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res?.releases).toMatchObject([ + { version: 'v1.0.0' }, + { version: 'v1.0.1' }, + ]); + }); + + it('handles pipe fallback', async () => { + process.env.GOPROXY = `https://example.com|${baseUrl}`; + + httpMock + .scope('https://example.com/github.com/google/btree') + .get('/@v/list') + .replyWithError('unknown'); + + httpMock + .scope(`${baseUrl}/github.com/google/btree`) + .get('/@v/list') + .reply(200, 'v1.0.0\nv1.0.1\n') + .get('/@v/v1.0.0.info') + .reply(200, { Version: 'v1.0.0', Time: '2018-08-13T15:31:12Z' }) + .get('/@v/v1.0.1.info') + .reply(200, { Version: 'v1.0.1', Time: '2019-10-16T16:15:28Z' }); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res?.releases).toMatchObject([ + { releaseTimestamp: '2018-08-13T15:31:12.000Z', version: 'v1.0.0' }, + { releaseTimestamp: '2019-10-16T16:15:28.000Z', version: 'v1.0.1' }, + ]); + }); + + it('handles comma fallback', async () => { + process.env.GOPROXY = [ + 'https://foo.example.com', + 'https://bar.example.com', + baseUrl, + ].join(','); + + httpMock + .scope('https://foo.example.com/github.com/google/btree') + .get('/@v/list') + .reply(404); + + httpMock + .scope('https://bar.example.com/github.com/google/btree') + .get('/@v/list') + .reply(410); + + httpMock + .scope(`${baseUrl}/github.com/google/btree`) + .get('/@v/list') + .reply(200, 'v1.0.0\nv1.0.1\n') + .get('/@v/v1.0.0.info') + .reply(200, { Version: 'v1.0.0', Time: '2018-08-13T15:31:12Z' }) + .get('/@v/v1.0.1.info') + .reply(200, { Version: 'v1.0.1', Time: '2019-10-16T16:15:28Z' }); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res?.releases).toMatchObject([ + { releaseTimestamp: '2018-08-13T15:31:12.000Z', version: 'v1.0.0' }, + { releaseTimestamp: '2019-10-16T16:15:28.000Z', version: 'v1.0.1' }, + ]); + }); + + it('short-circuits with comma fallback', async () => { + process.env.GOPROXY = [ + 'https://foo.example.com', + 'https://bar.example.com', + 'https://baz.example.com', + baseUrl, + ].join(','); + + httpMock + .scope('https://foo.example.com/github.com/google/btree') + .get('/@v/list') + .reply(404); + + httpMock + .scope('https://bar.example.com/github.com/google/btree') + .get('/@v/list') + .reply(410); + + httpMock + .scope('https://baz.example.com/github.com/google/btree') + .get('/@v/list') + .replyWithError('unknown'); + + httpMock + .scope('https://api.github.com/') + .get('/repos/google/btree/tags?per_page=100') + .reply(200, [{ name: 'v1.0.0' }, { name: 'v1.0.1' }]) + .get('/repos/google/btree/releases?per_page=100') + .reply(200, []); + + const res = await getPkgReleases({ + datasource, + depName: 'github.com/google/btree', + }); + expect(httpMock.getTrace()).toMatchSnapshot(); + expect(res).toEqual({ + releases: [ + { gitRef: 'v1.0.0', version: 'v1.0.0' }, + { gitRef: 'v1.0.1', version: 'v1.0.1' }, + ], + sourceUrl: 'https://github.com/google/btree', + }); + }); + }); }); }); diff --git a/lib/datasource/go/index.ts b/lib/datasource/go/index.ts index 527473d89ce9a9d626a7ebb58d670eb3d548c3ea..7dd40a0577678d6f4e55b2f6ce985830054d74aa 100644 --- a/lib/datasource/go/index.ts +++ b/lib/datasource/go/index.ts @@ -2,20 +2,21 @@ import URL from 'url'; import { PLATFORM_TYPE_GITLAB } from '../../constants/platforms'; import { logger } from '../../logger'; import * as hostRules from '../../util/host-rules'; -import { Http } from '../../util/http'; import { regEx } from '../../util/regex'; import { trimTrailingSlash } from '../../util/url'; import { BitBucketTagsDatasource } from '../bitbucket-tags'; import * as github from '../github-tags'; import * as gitlab from '../gitlab-tags'; import type { DigestConfig, GetReleasesConfig, ReleaseResult } from '../types'; +import { http } from './common'; +import * as goproxy from './goproxy'; import type { DataSource } from './types'; -export const id = 'go'; +export { id } from './common'; + export const customRegistrySupport = false; -const http = new Http(id); -const gitlabRegExp = /^(https:\/\/[^/]*gitlab.[^/]*)\/(.*)$/; +const gitlabRegExp = /^(https:\/\/[^/]*gitlab\.[^/]*)\/(.*)$/; const bitbucket = new BitBucketTagsDatasource(); async function getDatasource(goModule: string): Promise<DataSource | null> { @@ -142,9 +143,19 @@ async function getDatasource(goModule: string): Promise<DataSource | null> { * - Call the respective getReleases in github/gitlab to retrieve the tags * - Filter module tags according to the module path */ -export async function getReleases({ - lookupName, -}: GetReleasesConfig): Promise<ReleaseResult | null> { +export async function getReleases( + config: GetReleasesConfig +): Promise<ReleaseResult | null> { + const { lookupName } = config; + + let res: ReleaseResult = null; + + logger.trace(`goproxy.getReleases(${lookupName})`); + res = await goproxy.getReleases(config); + if (res) { + return res; + } + logger.trace(`go.getReleases(${lookupName})`); const source = await getDatasource(lookupName); @@ -156,8 +167,6 @@ export async function getReleases({ return null; } - let res: ReleaseResult; - switch (source.datasource) { case github.id: { res = await github.getReleases(source); diff --git a/lib/datasource/go/types.ts b/lib/datasource/go/types.ts index c333c22e6a592f51819ce166408a09cb2c692f98..8eabe0696c0bdc00f36aec604035e7b4e96ac0b1 100644 --- a/lib/datasource/go/types.ts +++ b/lib/datasource/go/types.ts @@ -1,5 +1,17 @@ +import type { GoproxyFallback } from './common'; + export interface DataSource { datasource: string; registryUrl?: string; lookupName: string; } + +export interface VersionInfo { + Version: string; + Time?: string; +} + +export interface GoproxyItem { + url: string; + fallback: GoproxyFallback; +}