diff --git a/lib/modules/manager/bazel/common.ts b/lib/modules/manager/bazel/common.ts deleted file mode 100644 index 7bb8c95b4f5f5b73f66511594e1fa8167653b1e4..0000000000000000000000000000000000000000 --- a/lib/modules/manager/bazel/common.ts +++ /dev/null @@ -1,287 +0,0 @@ -import is from '@sindresorhus/is'; -import parseGithubUrl from 'github-url-from-git'; -import { logger } from '../../../logger'; -import { regEx } from '../../../util/regex'; -import { parseUrl } from '../../../util/url'; -import { DockerDatasource } from '../../datasource/docker'; -import { GithubReleasesDatasource } from '../../datasource/github-releases'; -import { GithubTagsDatasource } from '../../datasource/github-tags'; -import { GoDatasource } from '../../datasource/go'; -import { id as dockerVersioning } from '../../versioning/docker'; -import type { PackageDependency } from '../types'; -import type { - Fragment, - StringFragment, - Target, - TargetAttribute, - UrlParsedResult, -} from './types'; - -function isTarget(x: Record<string, TargetAttribute>): x is Target { - return is.string(x.name) && is.string(x.rule); -} - -export function coerceFragmentToTarget(fragment: Fragment): Target | null { - if (fragment.type === 'record') { - const { children } = fragment; - const target: Record<string, TargetAttribute> = {}; - for (const [key, value] of Object.entries(children)) { - if (value.type === 'array') { - const values = value.children - .filter((x): x is StringFragment => x.type === 'string') - .map((x) => x.value); - target[key] = values; - } else if (value.type === 'string') { - target[key] = value.value; - } - } - - if (isTarget(target)) { - return target; - } - } - - return null; -} - -export function parseArchiveUrl( - urlString: string | undefined | null -): UrlParsedResult | null { - if (!urlString) { - return null; - } - const url = parseUrl(urlString); - if (!url || url.host !== 'github.com' || !url.pathname) { - return null; - } - const path = url.pathname.split('/').slice(1); - const repo = path[0] + '/' + path[1]; - let datasource = ''; - let currentValue: string | null = null; - if (path[2] === 'releases' && path[3] === 'download') { - datasource = GithubReleasesDatasource.id; - currentValue = path[4]; - } else if ( - path[2] === 'archive' && - path[3] === 'refs' && - path[4] === 'tags' - ) { - datasource = GithubTagsDatasource.id; - currentValue = path[5]; - } else if (path[2] === 'archive') { - datasource = GithubTagsDatasource.id; - currentValue = path[3]; - } - - if (currentValue) { - // Strip archive extension to get hash or tag. - // Tolerates formats produced by Git(Hub|Lab) and allowed by http_archive - // Note: Order matters in suffix list to strip, e.g. .tar.gz. - for (const extension of ['.gz', '.bz2', '.xz', '.tar', '.tgz', '.zip']) { - if (currentValue.endsWith(extension)) { - currentValue = currentValue.slice(0, -extension.length); - } - } - - return { datasource, repo, currentValue }; - } - return null; -} - -export function gitDependency({ - rule: depType, - name: depName, - tag: currentValue, - commit: currentDigest, - remote, -}: Target): PackageDependency | null { - let dep: PackageDependency | null = null; - - if ( - depType === 'git_repository' && - is.string(depName) && - (is.string(currentValue) || is.string(currentDigest)) && - is.string(remote) - ) { - dep = { - datasource: GithubReleasesDatasource.id, - depType, - depName, - }; - - if (is.string(currentValue)) { - dep.currentValue = currentValue; - } - - if (is.string(currentDigest)) { - dep.currentDigest = currentDigest; - } - - // TODO: Check if we really need to use parse here or if it should always be a plain https url (#9605) - const packageName = parseGithubUrl(remote)?.substring( - 'https://github.com/'.length - ); - - // istanbul ignore else - if (packageName) { - dep.packageName = packageName; - } else { - dep.skipReason = 'unsupported-remote'; - } - } - - return dep; -} - -export function goDependency({ - rule: depType, - name: depName, - tag: currentValue, - commit: currentDigest, - importpath: packageName, - remote, -}: Target): PackageDependency | null { - let dep: PackageDependency | null = null; - - if ( - depType === 'go_repository' && - is.string(depName) && - (is.string(currentValue) || is.string(currentDigest)) && - is.string(packageName) - ) { - dep = { - datasource: GoDatasource.id, - depType, - depName, - packageName, - }; - - if (is.string(currentValue)) { - dep.currentValue = currentValue; - } - - if (is.string(currentDigest)) { - dep.currentValue = 'v0.0.0'; - dep.currentDigest = currentDigest; - dep.currentDigestShort = currentDigest.substring(0, 7); - dep.digestOneAndOnly = true; - } - - if (is.string(remote)) { - const remoteMatch = regEx( - /https:\/\/github\.com(?:.*\/)(([a-zA-Z]+)([-])?([a-zA-Z]+))/ - ).exec(remote); - if (remoteMatch && remoteMatch[0].length === remote.length) { - dep.packageName = remote.replace('https://', ''); - } else { - dep.skipReason = 'unsupported-remote'; - } - } - } - - return dep; -} - -export function httpDependency({ - rule: depType, - name: depName, - url, - urls, - sha256, -}: Target): PackageDependency | null { - let dep: PackageDependency | null = null; - - if ( - (depType === 'http_archive' || depType === 'http_file') && - is.string(depName) && - is.string(sha256) - ) { - let parsedUrl: UrlParsedResult | null = null; - if (is.string(url)) { - parsedUrl = parseArchiveUrl(url); - } else if (is.array(urls, is.string)) { - for (const u of urls) { - parsedUrl = parseArchiveUrl(u); - if (parsedUrl) { - break; - } - } - } - - if (parsedUrl) { - dep = { - datasource: parsedUrl.datasource, - depType, - depName, - packageName: parsedUrl.repo, - }; - - if (regEx(/^[a-f0-9]{40}$/i).test(parsedUrl.currentValue)) { - dep.currentDigest = parsedUrl.currentValue; - } else { - dep.currentValue = parsedUrl.currentValue; - } - } - } - - return dep; -} - -export function dockerDependency({ - rule: depType, - name: depName, - tag: currentValue, - digest: currentDigest, - repository: packageName, - registry, -}: Target): PackageDependency | null { - let dep: PackageDependency | null = null; - - if ( - depType === 'container_pull' && - is.string(depName) && - is.string(currentValue) && - is.string(currentDigest) && - is.string(packageName) && - is.string(registry) - ) { - dep = { - datasource: DockerDatasource.id, - versioning: dockerVersioning, - depType, - depName, - packageName, - currentValue, - currentDigest, - registryUrls: [registry], - }; - } - - return dep; -} - -type DependencyExtractor = (_: Target) => PackageDependency | null; -type DependencyExtractorRegistry = Record<string, DependencyExtractor>; - -const dependencyExtractorRegistry: DependencyExtractorRegistry = { - git_repository: gitDependency, - go_repository: goDependency, - http_archive: httpDependency, - http_file: httpDependency, - container_pull: dockerDependency, -}; - -const supportedRules = Object.keys(dependencyExtractorRegistry); - -export const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); - -export function extractDepFromTarget(target: Target): PackageDependency | null { - const dependencyExtractor = dependencyExtractorRegistry[target.rule]; - if (!dependencyExtractor) { - logger.debug( - `Bazel dependency extractor function not found for ${target.rule}` - ); - return null; - } - return dependencyExtractor(target); -} diff --git a/lib/modules/manager/bazel/extract.ts b/lib/modules/manager/bazel/extract.ts index 15ef0d970852b20d22cdb499a0c5eefc3fd8d086..f266ded95fa31b2caa6fbf8631e8d0e842519d74 100644 --- a/lib/modules/manager/bazel/extract.ts +++ b/lib/modules/manager/bazel/extract.ts @@ -1,7 +1,7 @@ import { logger } from '../../../logger'; import type { PackageDependency, PackageFile } from '../types'; -import { coerceFragmentToTarget, extractDepFromTarget } from './common'; import { parse } from './parser'; +import { extractDepFromFragment } from './rules'; import type { ArrayFragment } from './types'; export function extractPackageFile( @@ -23,12 +23,8 @@ export function extractPackageFile( for (let idx = 0; idx < parsed.children.length; idx += 1) { const fragment = parsed.children[idx]; - const target = coerceFragmentToTarget(fragment); - if (!target) { - continue; - } - const dep = extractDepFromTarget(target); + const dep = extractDepFromFragment(fragment); if (!dep) { continue; } diff --git a/lib/modules/manager/bazel/parser.ts b/lib/modules/manager/bazel/parser.ts index 6fcb7ce21b8652187cabe0d543eda7f7600810d8..70f5090145b364be9e3fb47862c079e5e5a04151 100644 --- a/lib/modules/manager/bazel/parser.ts +++ b/lib/modules/manager/bazel/parser.ts @@ -1,7 +1,7 @@ import { lang, lexer, parser, query as q } from 'good-enough-parser'; import hasha from 'hasha'; import * as memCache from '../../../util/cache/memory'; -import { supportedRulesRegex } from './common'; +import { supportedRulesRegex } from './rules/index'; import type { ArrayFragment, NestedFragment, RecordFragment } from './types'; interface Ctx { diff --git a/lib/modules/manager/bazel/rules/docker.ts b/lib/modules/manager/bazel/rules/docker.ts new file mode 100644 index 0000000000000000000000000000000000000000..797c8c013562f5fbbec5c96edf55e596bc2d3e93 --- /dev/null +++ b/lib/modules/manager/bazel/rules/docker.ts @@ -0,0 +1,38 @@ +import is from '@sindresorhus/is'; +import { DockerDatasource } from '../../../datasource/docker'; +import { id as dockerVersioning } from '../../../versioning/docker'; +import type { PackageDependency } from '../../types'; +import type { Target } from '../types'; + +export function dockerDependency({ + rule: depType, + name: depName, + tag: currentValue, + digest: currentDigest, + repository: packageName, + registry, +}: Target): PackageDependency | null { + let dep: PackageDependency | null = null; + + if ( + depType === 'container_pull' && + is.string(depName) && + is.string(currentValue) && + is.string(currentDigest) && + is.string(packageName) && + is.string(registry) + ) { + dep = { + datasource: DockerDatasource.id, + versioning: dockerVersioning, + depType, + depName, + packageName, + currentValue, + currentDigest, + registryUrls: [registry], + }; + } + + return dep; +} diff --git a/lib/modules/manager/bazel/rules/git.ts b/lib/modules/manager/bazel/rules/git.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5ee9affb5476dd1634351cdf5a65d68dd8cd477 --- /dev/null +++ b/lib/modules/manager/bazel/rules/git.ts @@ -0,0 +1,50 @@ +import is from '@sindresorhus/is'; +import parseGithubUrl from 'github-url-from-git'; +import { GithubReleasesDatasource } from '../../../datasource/github-releases'; +import type { PackageDependency } from '../../types'; +import type { Target } from '../types'; + +export function gitDependency({ + rule: depType, + name: depName, + tag: currentValue, + commit: currentDigest, + remote, +}: Target): PackageDependency | null { + let dep: PackageDependency | null = null; + + if ( + depType === 'git_repository' && + is.string(depName) && + (is.string(currentValue) || is.string(currentDigest)) && + is.string(remote) + ) { + dep = { + datasource: GithubReleasesDatasource.id, + depType, + depName, + }; + + if (is.string(currentValue)) { + dep.currentValue = currentValue; + } + + if (is.string(currentDigest)) { + dep.currentDigest = currentDigest; + } + + // TODO: Check if we really need to use parse here or if it should always be a plain https url (#9605) + const packageName = parseGithubUrl(remote)?.substring( + 'https://github.com/'.length + ); + + // istanbul ignore else + if (packageName) { + dep.packageName = packageName; + } else { + dep.skipReason = 'unsupported-remote'; + } + } + + return dep; +} diff --git a/lib/modules/manager/bazel/rules/go.ts b/lib/modules/manager/bazel/rules/go.ts new file mode 100644 index 0000000000000000000000000000000000000000..d82036fe04d4119f1fe42d276687bc724dcde41a --- /dev/null +++ b/lib/modules/manager/bazel/rules/go.ts @@ -0,0 +1,54 @@ +import is from '@sindresorhus/is'; +import { regEx } from '../../../../util/regex'; +import { GoDatasource } from '../../../datasource/go'; +import type { PackageDependency } from '../../types'; +import type { Target } from '../types'; + +export function goDependency({ + rule: depType, + name: depName, + tag: currentValue, + commit: currentDigest, + importpath: packageName, + remote, +}: Target): PackageDependency | null { + let dep: PackageDependency | null = null; + + if ( + depType === 'go_repository' && + is.string(depName) && + (is.string(currentValue) || is.string(currentDigest)) && + is.string(packageName) + ) { + dep = { + datasource: GoDatasource.id, + depType, + depName, + packageName, + }; + + if (is.string(currentValue)) { + dep.currentValue = currentValue; + } + + if (is.string(currentDigest)) { + dep.currentValue = 'v0.0.0'; + dep.currentDigest = currentDigest; + dep.currentDigestShort = currentDigest.substring(0, 7); + dep.digestOneAndOnly = true; + } + + if (is.string(remote)) { + const remoteMatch = regEx( + /https:\/\/github\.com(?:.*\/)(([a-zA-Z]+)([-])?([a-zA-Z]+))/ + ).exec(remote); + if (remoteMatch && remoteMatch[0].length === remote.length) { + dep.packageName = remote.replace('https://', ''); + } else { + dep.skipReason = 'unsupported-remote'; + } + } + } + + return dep; +} diff --git a/lib/modules/manager/bazel/rules/http.ts b/lib/modules/manager/bazel/rules/http.ts new file mode 100644 index 0000000000000000000000000000000000000000..10dbcfbe03aa9f11d65aecc622c645257dc79293 --- /dev/null +++ b/lib/modules/manager/bazel/rules/http.ts @@ -0,0 +1,96 @@ +import is from '@sindresorhus/is'; +import { regEx } from '../../../../util/regex'; +import { parseUrl } from '../../../../util/url'; +import { GithubReleasesDatasource } from '../../../datasource/github-releases'; +import { GithubTagsDatasource } from '../../../datasource/github-tags'; +import type { PackageDependency } from '../../types'; +import type { Target, UrlParsedResult } from '../types'; + +export function parseArchiveUrl( + urlString: string | undefined | null +): UrlParsedResult | null { + if (!urlString) { + return null; + } + const url = parseUrl(urlString); + if (!url || url.host !== 'github.com' || !url.pathname) { + return null; + } + const path = url.pathname.split('/').slice(1); + const repo = path[0] + '/' + path[1]; + let datasource = ''; + let currentValue: string | null = null; + if (path[2] === 'releases' && path[3] === 'download') { + datasource = GithubReleasesDatasource.id; + currentValue = path[4]; + } else if ( + path[2] === 'archive' && + path[3] === 'refs' && + path[4] === 'tags' + ) { + datasource = GithubTagsDatasource.id; + currentValue = path[5]; + } else if (path[2] === 'archive') { + datasource = GithubTagsDatasource.id; + currentValue = path[3]; + } + + if (currentValue) { + // Strip archive extension to get hash or tag. + // Tolerates formats produced by Git(Hub|Lab) and allowed by http_archive + // Note: Order matters in suffix list to strip, e.g. .tar.gz. + for (const extension of ['.gz', '.bz2', '.xz', '.tar', '.tgz', '.zip']) { + if (currentValue.endsWith(extension)) { + currentValue = currentValue.slice(0, -extension.length); + } + } + + return { datasource, repo, currentValue }; + } + return null; +} + +export function httpDependency({ + rule: depType, + name: depName, + url, + urls, + sha256, +}: Target): PackageDependency | null { + let dep: PackageDependency | null = null; + + if ( + (depType === 'http_archive' || depType === 'http_file') && + is.string(depName) && + is.string(sha256) + ) { + let parsedUrl: UrlParsedResult | null = null; + if (is.string(url)) { + parsedUrl = parseArchiveUrl(url); + } else if (is.array(urls, is.string)) { + for (const u of urls) { + parsedUrl = parseArchiveUrl(u); + if (parsedUrl) { + break; + } + } + } + + if (parsedUrl) { + dep = { + datasource: parsedUrl.datasource, + depType, + depName, + packageName: parsedUrl.repo, + }; + + if (regEx(/^[a-f0-9]{40}$/i).test(parsedUrl.currentValue)) { + dep.currentDigest = parsedUrl.currentValue; + } else { + dep.currentValue = parsedUrl.currentValue; + } + } + } + + return dep; +} diff --git a/lib/modules/manager/bazel/common.spec.ts b/lib/modules/manager/bazel/rules/index.spec.ts similarity index 80% rename from lib/modules/manager/bazel/common.spec.ts rename to lib/modules/manager/bazel/rules/index.spec.ts index 10c25c67fe0ba2322859e59c79b2eba294895984..f0b268641db0831d9687b02bafe3f043135b6169 100644 --- a/lib/modules/manager/bazel/common.spec.ts +++ b/lib/modules/manager/bazel/rules/index.spec.ts @@ -1,13 +1,10 @@ -import { - dockerDependency, - extractDepFromTarget, - gitDependency, - goDependency, - httpDependency, - parseArchiveUrl, -} from './common'; +import { dockerDependency } from './docker'; +import { gitDependency } from './git'; +import { goDependency } from './go'; +import { httpDependency, parseArchiveUrl } from './http'; +import { extractDepFromFragment } from '.'; -describe('modules/manager/bazel/common', () => { +describe('modules/manager/bazel/rules/index', () => { test('parseUrl', () => { expect(parseArchiveUrl('')).toBeNull(); expect(parseArchiveUrl(null)).toBeNull(); @@ -291,18 +288,37 @@ describe('modules/manager/bazel/common', () => { }); }); - describe('extractDepFromTarget', () => { + describe('extractDepFromFragment', () => { it('returns null for unknown rule type', () => { - expect(extractDepFromTarget({ rule: 'foo', name: 'bar' })).toBeNull(); + expect( + extractDepFromFragment({ + type: 'record', + value: '', + offset: 0, + children: { + rule: { type: 'string', value: 'foo', offset: 0 }, + name: { type: 'string', value: 'bar', offset: 0 }, + }, + }) + ).toBeNull(); }); it('extracts from git_repository', () => { expect( - extractDepFromTarget({ - rule: 'git_repository', - name: 'foo_bar', - tag: '1.2.3', - remote: 'https://github.com/foo/bar', + extractDepFromFragment({ + type: 'record', + value: '', + offset: 0, + children: { + rule: { type: 'string', value: 'git_repository', offset: 0 }, + name: { type: 'string', value: 'foo_bar', offset: 0 }, + tag: { type: 'string', value: '1.2.3', offset: 0 }, + remote: { + type: 'string', + value: 'https://github.com/foo/bar', + offset: 0, + }, + }, }) ).toEqual({ datasource: 'github-releases', @@ -315,14 +331,33 @@ describe('modules/manager/bazel/common', () => { it('extracts from http_archive', () => { expect( - extractDepFromTarget({ - rule: 'http_archive', - name: 'rules_nodejs', - sha256: - '5aef09ed3279aa01d5c928e3beb248f9ad32dde6aafe6373a8c994c3ce643064', - urls: [ - 'https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.3/rules_nodejs-core-5.5.3.tar.gz', - ], + extractDepFromFragment({ + type: 'record', + value: '', + offset: 0, + children: { + rule: { type: 'string', value: 'http_archive', offset: 0 }, + name: { type: 'string', value: 'rules_nodejs', offset: 0 }, + sha256: { + type: 'string', + value: + '5aef09ed3279aa01d5c928e3beb248f9ad32dde6aafe6373a8c994c3ce643064', + offset: 0, + }, + urls: { + type: 'array', + value: '', + offset: 0, + children: [ + { + type: 'string', + offset: 0, + value: + 'https://github.com/bazelbuild/rules_nodejs/releases/download/5.5.3/rules_nodejs-core-5.5.3.tar.gz', + }, + ], + }, + }, }) ).toEqual({ datasource: 'github-releases', diff --git a/lib/modules/manager/bazel/rules/index.ts b/lib/modules/manager/bazel/rules/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec9c7966541da2580e97b5fe06db3fd342371e3e --- /dev/null +++ b/lib/modules/manager/bazel/rules/index.ts @@ -0,0 +1,74 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../../logger'; +import { regEx } from '../../../../util/regex'; +import type { PackageDependency } from '../../types'; +import type { + Fragment, + StringFragment, + Target, + TargetAttribute, +} from '../types'; +import { dockerDependency } from './docker'; +import { gitDependency } from './git'; +import { goDependency } from './go'; +import { httpDependency } from './http'; + +type DependencyExtractor = (_: Target) => PackageDependency | null; +type DependencyExtractorRegistry = Record<string, DependencyExtractor>; + +const dependencyExtractorRegistry: DependencyExtractorRegistry = { + git_repository: gitDependency, + go_repository: goDependency, + http_archive: httpDependency, + http_file: httpDependency, + container_pull: dockerDependency, +}; + +const supportedRules = Object.keys(dependencyExtractorRegistry); +export const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); + +function isTarget(x: Record<string, TargetAttribute>): x is Target { + return is.string(x.name) && is.string(x.rule); +} + +export function coerceFragmentToTarget(fragment: Fragment): Target | null { + if (fragment.type === 'record') { + const { children } = fragment; + const target: Record<string, TargetAttribute> = {}; + for (const [key, value] of Object.entries(children)) { + if (value.type === 'array') { + const values = value.children + .filter((x): x is StringFragment => x.type === 'string') + .map((x) => x.value); + target[key] = values; + } else if (value.type === 'string') { + target[key] = value.value; + } + } + + if (isTarget(target)) { + return target; + } + } + + return null; +} + +export function extractDepFromFragment( + fragment: Fragment +): PackageDependency | null { + const target = coerceFragmentToTarget(fragment); + if (!target) { + return null; + } + + const dependencyExtractor = dependencyExtractorRegistry[target.rule]; + if (!dependencyExtractor) { + logger.debug( + `Bazel dependency extractor function not found for ${target.rule}` + ); + return null; + } + + return dependencyExtractor(target); +}