diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts deleted file mode 100644 index 2b4c022621910271972807b5b9013b14af4fdb0f..0000000000000000000000000000000000000000 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { BazelDatasource } from '../../datasource/bazel'; -import { ToBazelDep } from './bazel-dep'; -import * as fragments from './fragments'; - -describe('modules/manager/bazel-module/bazel-dep', () => { - describe('ToBazelDep', () => { - it('transforms a record fragment', () => { - const record = fragments.record({ - rule: fragments.string('bazel_dep'), - name: fragments.string('rules_foo'), - version: fragments.string('1.2.3'), - dev_dependency: fragments.boolean(true), - }); - const result = ToBazelDep.parse(record); - expect(result).toEqual({ - datasource: BazelDatasource.id, - depType: 'bazel_dep', - depName: 'rules_foo', - currentValue: '1.2.3', - }); - }); - }); -}); diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts deleted file mode 100644 index 8d6f6c85e6cb14a8c296da1e923177db4b096f9a..0000000000000000000000000000000000000000 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { z } from 'zod'; -import { BazelDatasource } from '../../datasource/bazel'; -import type { PackageDependency } from '../types'; -import { - BooleanFragmentSchema, - RecordFragmentSchema, - StringFragmentSchema, -} from './fragments'; - -const BazelDepSchema = RecordFragmentSchema.extend({ - children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('bazel_dep'), - }), - name: StringFragmentSchema, - version: StringFragmentSchema, - dev_dependency: BooleanFragmentSchema.optional(), - }), -}); - -export const ToBazelDep = BazelDepSchema.transform( - ({ children: { rule, name, version } }): PackageDependency => ({ - datasource: BazelDatasource.id, - depType: rule.value, - depName: name.value, - currentValue: version.value, - }) -); diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 8207d4e0f5dcf8fdf1b5d1e89577f3e8cc012569..3e6481e4fed1da89c5efbb9ed355cfb16f46a4e9 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,5 +1,6 @@ import { codeBlock } from 'common-tags'; import { BazelDatasource } from '../../datasource/bazel'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; import * as parser from './parser'; import { extractPackageFile } from '.'; @@ -54,5 +55,46 @@ describe('modules/manager/bazel-module/extract', () => { ], }); }); + + it('returns bazel_dep and git_override dependencies', () => { + const input = codeBlock` + bazel_dep(name = "rules_foo", version = "1.2.3") + bazel_dep(name = "rules_bar", version = "1.0.0", dev_dependency = True) + git_override( + module_name = "rules_foo", + remote = "https://github.com/example/rules_foo.git", + commit = "850cb49c8649e463b80ef7984e7c744279746170", + ) + `; + const result = extractPackageFile(input, 'MODULE.bazel'); + if (!result) { + throw new Error('Expected a result.'); + } + expect(result.deps).toHaveLength(3); + expect(result.deps).toEqual( + expect.arrayContaining([ + { + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_bar', + currentValue: '1.0.0', + }, + { + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_foo', + currentValue: '1.2.3', + skipReason: 'git-dependency', + }, + { + datasource: GithubTagsDatasource.id, + depType: 'git_override', + depName: 'rules_foo', + currentDigest: '850cb49c8649e463b80ef7984e7c744279746170', + packageName: 'example/rules_foo', + }, + ]) + ); + }); }); }); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index cc6eebf8647fc60e34a8c4a154f3db1252d94332..9ab2b4822d4d2e6b7af45fa07488739b076ed310 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,8 +1,9 @@ import { logger } from '../../../logger'; import { LooseArray } from '../../../util/schema-utils'; import type { PackageFileContent } from '../types'; -import { ToBazelDep } from './bazel-dep'; import { parse } from './parser'; +import { RuleToBazelModulePackageDep } from './rules'; +import * as rules from './rules'; export function extractPackageFile( content: string, @@ -10,7 +11,8 @@ export function extractPackageFile( ): PackageFileContent | null { try { const records = parse(content); - return LooseArray(ToBazelDep) + return LooseArray(RuleToBazelModulePackageDep) + .transform(rules.toPackageDependencies) .transform((deps) => (deps.length ? { deps } : null)) .parse(records); } catch (err) { diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts index 3312b93fe6573d13060d1ce63b5a0ed5270407cf..17cbb22768310633fb394c472c95c73c5e41aee3 100644 --- a/lib/modules/manager/bazel-module/index.ts +++ b/lib/modules/manager/bazel-module/index.ts @@ -1,4 +1,5 @@ import { BazelDatasource } from '../../datasource/bazel'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; import { extractPackageFile } from './extract'; export { extractPackageFile }; @@ -11,4 +12,7 @@ export const defaultConfig = { enabled: false, }; -export const supportedDatasources = [BazelDatasource.id]; +export const supportedDatasources = [ + BazelDatasource.id, + GithubTagsDatasource.id, +]; diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index 56392fef8a50ec8a5e650c2b125017b94e61182d..2a31b8efa66f06a051ceded7c9ef8b4328c1b272 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -40,5 +40,42 @@ describe('modules/manager/bazel-module/parser', () => { ), ]); }); + + it('finds the git_override', () => { + const input = codeBlock` + bazel_dep(name = "rules_foo", version = "1.2.3") + git_override( + module_name = "rules_foo", + remote = "https://github.com/example/rules_foo.git", + commit = "6a2c2e22849b3e6b33d5ea9aa72222d4803a986a", + patches = ["//:rules_foo.patch"], + patch_strip = 1, + ) + `; + const res = parse(input); + expect(res).toEqual([ + fragments.record( + { + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_foo'), + version: fragments.string('1.2.3'), + }, + true + ), + fragments.record( + { + rule: fragments.string('git_override'), + module_name: fragments.string('rules_foo'), + remote: fragments.string( + 'https://github.com/example/rules_foo.git' + ), + commit: fragments.string( + '6a2c2e22849b3e6b33d5ea9aa72222d4803a986a' + ), + }, + true + ), + ]); + }); }); }); diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 6a33772183e94224017bfd7888ad3369e8012774..c9bf6ee7970ef4e25c48e8a2e75c0b1b9404098d 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -5,13 +5,12 @@ import type { RecordFragment } from './fragments'; import * as starlark from './starlark'; const booleanValuesRegex = regEx(`^${starlark.booleanStringValues.join('|')}$`); -const supportedRules = ['bazel_dep']; +const supportedRules = ['bazel_dep', 'git_override']; const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); /** * Matches key-value pairs: * - `name = "foobar"` - * - `dev_dependeny = True` **/ const kvParams = q .sym<Ctx>((ctx, token) => ctx.startAttribute(token.value)) diff --git a/lib/modules/manager/bazel-module/rules.spec.ts b/lib/modules/manager/bazel-module/rules.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9c1b06c105cfb25861c9f033726a7b6d36aeea5 --- /dev/null +++ b/lib/modules/manager/bazel-module/rules.spec.ts @@ -0,0 +1,125 @@ +import deepmerge from 'deepmerge'; +import { BazelDatasource } from '../../datasource/bazel'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import type { PackageDependency } from '../types'; +import * as fragments from './fragments'; +import { + BasePackageDep, + BazelModulePackageDep, + OverridePackageDep, + RuleToBazelModulePackageDep, + overrideToPackageDependency, + processModulePkgDeps, + toPackageDependencies, +} from './rules'; + +const bazelDepPkgDep: BasePackageDep = { + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_foo', + currentValue: '1.2.3', +}; +const gitOverrideForGithubPkgDep: OverridePackageDep = { + datasource: GithubTagsDatasource.id, + depType: 'git_override', + depName: 'rules_foo', + packageName: 'example/rules_foo', + currentDigest: '850cb49c8649e463b80ef7984e7c744279746170', + bazelDepSkipReason: 'git-dependency', +}; +const gitOverrideForUnsupportedPkgDep: OverridePackageDep = { + depType: 'git_override', + depName: 'rules_foo', + currentDigest: '850cb49c8649e463b80ef7984e7c744279746170', + bazelDepSkipReason: 'git-dependency', + skipReason: 'unsupported-datasource', +}; + +describe('modules/manager/bazel-module/rules', () => { + describe('RuleToBazelModulePackageDep', () => { + const bazelDepWithoutDevDep = fragments.record({ + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_foo'), + version: fragments.string('1.2.3'), + }); + const gitOverrideWithGihubHost = fragments.record({ + rule: fragments.string('git_override'), + module_name: fragments.string('rules_foo'), + remote: fragments.string('https://github.com/example/rules_foo.git'), + commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), + }); + const gitOverrideWithUnsupportedHost = fragments.record({ + rule: fragments.string('git_override'), + module_name: fragments.string('rules_foo'), + remote: fragments.string('https://nobuenos.com/example/rules_foo.git'), + commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), + }); + + it.each` + msg | a | exp + ${'bazel_dep'} | ${bazelDepWithoutDevDep} | ${bazelDepPkgDep} + ${'git_override, GitHub host'} | ${gitOverrideWithGihubHost} | ${gitOverrideForGithubPkgDep} + ${'git_override, unsupported host'} | ${gitOverrideWithUnsupportedHost} | ${gitOverrideForUnsupportedPkgDep} + `('.parse() with $msg', ({ a, exp }) => { + const pkgDep = RuleToBazelModulePackageDep.parse(a); + expect(pkgDep).toEqual(exp); + }); + }); + + describe('.toPackageDependencies()', () => { + const expectedBazelDepNoOverrides: PackageDependency[] = [bazelDepPkgDep]; + const expectedBazelDepAndGitOverride: PackageDependency[] = [ + deepmerge(bazelDepPkgDep, { skipReason: 'git-dependency' }), + overrideToPackageDependency(gitOverrideForGithubPkgDep), + ]; + + it.each` + msg | a | exp + ${'bazel_dep, no overrides'} | ${[bazelDepPkgDep]} | ${expectedBazelDepNoOverrides} + ${'bazel_dep & git_override'} | ${[bazelDepPkgDep, gitOverrideForGithubPkgDep]} | ${expectedBazelDepAndGitOverride} + ${'git_override, no bazel_dep'} | ${[gitOverrideForGithubPkgDep]} | ${[]} + `('with $msg', ({ a, exp }) => { + const result = toPackageDependencies(a); + expect(result).toEqual(exp); + }); + }); + + describe('.overrideToPackageDependency()', () => { + it('removes the properties specific to OverridePackageDep', () => { + const result = overrideToPackageDependency(gitOverrideForGithubPkgDep); + expect(result).toEqual({ + datasource: GithubTagsDatasource.id, + depType: 'git_override', + depName: 'rules_foo', + packageName: 'example/rules_foo', + currentDigest: '850cb49c8649e463b80ef7984e7c744279746170', + }); + }); + }); + + describe('.processModulePkgDeps', () => { + it('returns an empty array if the input is an empty array', () => { + expect(processModulePkgDeps([])).toHaveLength(0); + }); + + it('returns the bazel_dep if more than one override is found', () => { + const bazelDep: BasePackageDep = { + depType: 'bazel_dep', + depName: 'rules_foo', + currentValue: '1.2.3', + }; + const override0: OverridePackageDep = { + depType: 'git_override', + depName: 'rules_foo', + bazelDepSkipReason: 'git-dependency', + }; + const override1: OverridePackageDep = { + depType: 'bar_override', + depName: 'rules_foo', + bazelDepSkipReason: 'unsupported-datasource', + }; + const pkgDeps: BazelModulePackageDep[] = [bazelDep, override0, override1]; + expect(processModulePkgDeps(pkgDeps)).toEqual([bazelDep]); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/rules.ts b/lib/modules/manager/bazel-module/rules.ts new file mode 100644 index 0000000000000000000000000000000000000000..f43adc66096e377245df22ed1611d0f1e5c95e62 --- /dev/null +++ b/lib/modules/manager/bazel-module/rules.ts @@ -0,0 +1,148 @@ +import is from '@sindresorhus/is'; +import parseGithubUrl from 'github-url-from-git'; +import { z } from 'zod'; +import { logger } from '../../../logger'; +import type { SkipReason } from '../../../types'; +import { regEx } from '../../../util/regex'; +import { BazelDatasource } from '../../datasource/bazel'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import type { PackageDependency } from '../types'; +import { RecordFragmentSchema, StringFragmentSchema } from './fragments'; + +// Rule Schemas + +export interface BasePackageDep extends PackageDependency { + depType: string; + depName: string; +} + +export interface OverridePackageDep extends BasePackageDep { + // This value is set as the skipReason on the bazel_dep PackageDependency. + bazelDepSkipReason: SkipReason; +} + +export type BazelModulePackageDep = BasePackageDep | OverridePackageDep; + +function isOverride(value: BazelModulePackageDep): value is OverridePackageDep { + return 'bazelDepSkipReason' in value; +} + +// This function exists to remove properties that are specific to +// OverridePackageDep. In theory, there is no harm in leaving the properties +// as it does not invalidate the PackageDependency interface. However, it might +// be surprising to someone outside the bazel-module code to see the extra +// properties. +export function overrideToPackageDependency( + override: OverridePackageDep +): PackageDependency { + const copy: Partial<OverridePackageDep> = { ...override }; + delete copy.bazelDepSkipReason; + return copy; +} + +const BazelDepToPackageDep = RecordFragmentSchema.extend({ + children: z.object({ + rule: StringFragmentSchema.extend({ + value: z.literal('bazel_dep'), + }), + name: StringFragmentSchema, + version: StringFragmentSchema, + }), +}).transform( + ({ children: { rule, name, version } }): BasePackageDep => ({ + datasource: BazelDatasource.id, + depType: rule.value, + depName: name.value, + currentValue: version.value, + }) +); + +const GitOverrideToPackageDep = RecordFragmentSchema.extend({ + children: z.object({ + rule: StringFragmentSchema.extend({ + value: z.literal('git_override'), + }), + module_name: StringFragmentSchema, + remote: StringFragmentSchema, + commit: StringFragmentSchema, + }), +}).transform( + ({ + children: { rule, module_name: moduleName, remote, commit }, + }): OverridePackageDep => { + const override: OverridePackageDep = { + depType: rule.value, + depName: moduleName.value, + bazelDepSkipReason: 'git-dependency', + currentDigest: commit.value, + }; + const ghPackageName = githubPackageName(remote.value); + if (is.nonEmptyString(ghPackageName)) { + override.datasource = GithubTagsDatasource.id; + override.packageName = ghPackageName; + } else { + override.skipReason = 'unsupported-datasource'; + } + return override; + } +); + +export const RuleToBazelModulePackageDep = z.union([ + BazelDepToPackageDep, + GitOverrideToPackageDep, +]); + +const githubRemoteRegex = regEx( + /^https:\/\/github\.com\/(?<packageName>[^/]+\/.+)$/ +); +function githubPackageName(remote: string): string | undefined { + return parseGithubUrl(remote)?.match(githubRemoteRegex)?.groups?.packageName; +} + +function collectByModule( + packageDeps: BazelModulePackageDep[] +): BazelModulePackageDep[][] { + const rulesByModule = new Map<string, BasePackageDep[]>(); + for (const pkgDep of packageDeps) { + const bmi = rulesByModule.get(pkgDep.depName) ?? []; + bmi.push(pkgDep); + rulesByModule.set(pkgDep.depName, bmi); + } + return Array.from(rulesByModule.values()); +} + +export function processModulePkgDeps( + packageDeps: BazelModulePackageDep[] +): PackageDependency[] { + if (!packageDeps.length) { + return []; + } + const moduleName = packageDeps[0].depName; + const bazelDep = packageDeps.find((pd) => pd.depType === 'bazel_dep'); + if (!bazelDep) { + logger.debug(`A 'bazel_dep' was not found for '${moduleName}'.`); + return []; + } + const deps: PackageDependency[] = [bazelDep]; + const overrides = packageDeps.filter(isOverride); + // It is an error for more than one override to exist for a module. We will + // ignore the overrides if there is more than one. + if (overrides.length !== 1) { + const depTypes = overrides.map((o) => o.depType); + logger.debug( + { depName: moduleName, depTypes }, + 'More than one override for a module was found' + ); + return deps; + } + const override = overrides[0]; + deps.push(overrideToPackageDependency(override)); + bazelDep.skipReason = override.bazelDepSkipReason; + return deps; +} + +export function toPackageDependencies( + packageDeps: BazelModulePackageDep[] +): PackageDependency[] { + return collectByModule(packageDeps).map(processModulePkgDeps).flat(); +}