Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bazel-modules): support single_version_override #22610

Merged
merged 13 commits into from
Jun 15, 2023
58 changes: 58 additions & 0 deletions lib/modules/manager/bazel-module/extract.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,63 @@ describe('modules/manager/bazel-module/extract', () => {
])
);
});

it('returns bazel_dep and single_version_override dependencies if a version is specified', async () => {
const input = codeBlock`
bazel_dep(name = "rules_foo", version = "1.2.3")
single_version_override(
module_name = "rules_foo",
version = "1.2.3",
registry = "https://example.com/custom_registry",
)
`;
const result = await extractPackageFile(input, 'MODULE.bazel');
if (!result) {
throw new Error('Expected a result.');
}
expect(result.deps).toHaveLength(2);
expect(result.deps).toEqual(
expect.arrayContaining([
{
datasource: BazelDatasource.id,
depType: 'bazel_dep',
depName: 'rules_foo',
currentValue: '1.2.3',
skipReason: 'is-pinned',
registryUrls: ['https://example.com/custom_registry'],
},
{
depType: 'single_version_override',
depName: 'rules_foo',
currentValue: '1.2.3',
skipReason: 'ignored',
registryUrls: ['https://example.com/custom_registry'],
},
])
);
});

it('returns bazel_dep dependency if single_version_override does not have a version', async () => {
const input = codeBlock`
bazel_dep(name = "rules_foo", version = "1.2.3")
single_version_override(
module_name = "rules_foo",
registry = "https://example.com/custom_registry",
)
`;
const result = await extractPackageFile(input, 'MODULE.bazel');
if (!result) {
throw new Error('Expected a result.');
}
expect(result.deps).toEqual([
{
datasource: BazelDatasource.id,
depType: 'bazel_dep',
depName: 'rules_foo',
currentValue: '1.2.3',
registryUrls: ['https://example.com/custom_registry'],
},
]);
});
});
});
31 changes: 31 additions & 0 deletions lib/modules/manager/bazel-module/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,36 @@ describe('modules/manager/bazel-module/parser', () => {
),
]);
});

it('finds single_version_override', () => {
const input = codeBlock`
bazel_dep(name = "rules_foo", version = "1.2.3")
single_version_override(
module_name = "rules_foo",
version = "1.2.3",
registry = "https://example.com/custom_registry",
)
`;
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('single_version_override'),
module_name: fragments.string('rules_foo'),
version: fragments.string('1.2.3'),
registry: fragments.string('https://example.com/custom_registry'),
},
true
),
]);
});
});
});
1 change: 1 addition & 0 deletions lib/modules/manager/bazel-module/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const supportedRules = [
'bazel_dep',
'git_override',
'local_path_override',
'single_version_override',
];
const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`);

Expand Down
99 changes: 69 additions & 30 deletions lib/modules/manager/bazel-module/rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import * as fragments from './fragments';
import {
BasePackageDep,
BazelModulePackageDep,
MergePackageDep,
OverridePackageDep,
RuleToBazelModulePackageDep,
overrideToPackageDependency,
bazelModulePackageDepToPackageDependency,
processModulePkgDeps,
toPackageDependencies,
} from './rules';

const customRegistryUrl = 'https://example.com/custom_registry';

const bazelDepPkgDep: BasePackageDep = {
datasource: BazelDatasource.id,
depType: 'bazel_dep',
Expand Down Expand Up @@ -46,6 +49,27 @@ const localPathOverridePkgDep: OverridePackageDep = {
skipReason: 'unsupported-datasource',
bazelDepSkipReason: 'local-dependency',
};
const singleVersionOverridePkgDep: OverridePackageDep & MergePackageDep = {
depType: 'single_version_override',
depName: 'rules_foo',
skipReason: 'ignored',
bazelDepSkipReason: 'is-pinned',
currentValue: '1.2.3',
bazelDepMergeFields: ['registryUrls'],
registryUrls: [customRegistryUrl],
};
const singleVersionOverrideWithRegistryPkgDep: MergePackageDep = {
depType: 'single_version_override',
depName: 'rules_foo',
skipReason: 'ignored',
bazelDepMergeFields: ['registryUrls'],
registryUrls: [customRegistryUrl],
};
const singleVersionOverrideWithoutVersionAndRegistryPkgDep: BasePackageDep = {
depType: 'single_version_override',
depName: 'rules_foo',
skipReason: 'ignored',
};

describe('modules/manager/bazel-module/rules', () => {
describe('RuleToBazelModulePackageDep', () => {
Expand Down Expand Up @@ -76,14 +100,27 @@ describe('modules/manager/bazel-module/rules', () => {
module_name: fragments.string('rules_foo'),
path: fragments.string('/path/to/module'),
});
const singleVersionOverride = fragments.record({
rule: fragments.string('single_version_override'),
module_name: fragments.string('rules_foo'),
version: fragments.string('1.2.3'),
registry: fragments.string(customRegistryUrl),
});
const singleVersionOverrideWithRegistry = fragments.record({
rule: fragments.string('single_version_override'),
module_name: fragments.string('rules_foo'),
registry: fragments.string(customRegistryUrl),
});

it.each`
msg | a | exp
${'bazel_dep'} | ${bazelDepWithoutDevDep} | ${bazelDepPkgDep}
${'git_override, GitHub host'} | ${gitOverrideWithGihubHost} | ${gitOverrideForGithubPkgDep}
${'git_override, unsupported host'} | ${gitOverrideWithUnsupportedHost} | ${gitOverrideForUnsupportedPkgDep}
${'archive_override'} | ${archiveOverride} | ${archiveOverridePkgDep}
${'local_path_override'} | ${localPathOverride} | ${localPathOverridePkgDep}
msg | a | exp
${'bazel_dep'} | ${bazelDepWithoutDevDep} | ${bazelDepPkgDep}
${'git_override, GitHub host'} | ${gitOverrideWithGihubHost} | ${gitOverrideForGithubPkgDep}
${'git_override, unsupported host'} | ${gitOverrideWithUnsupportedHost} | ${gitOverrideForUnsupportedPkgDep}
${'archive_override'} | ${archiveOverride} | ${archiveOverridePkgDep}
${'local_path_override'} | ${localPathOverride} | ${localPathOverridePkgDep}
${'single_version_override with version and registry'} | ${singleVersionOverride} | ${singleVersionOverridePkgDep}
${'single_version_override with registry'} | ${singleVersionOverrideWithRegistry} | ${singleVersionOverrideWithRegistryPkgDep}
`('.parse() with $msg', ({ a, exp }) => {
const pkgDep = RuleToBazelModulePackageDep.parse(a);
expect(pkgDep).toEqual(exp);
Expand All @@ -94,43 +131,45 @@ describe('modules/manager/bazel-module/rules', () => {
const expectedBazelDepNoOverrides: PackageDependency[] = [bazelDepPkgDep];
const expectedBazelDepAndGitOverride: PackageDependency[] = [
deepmerge(bazelDepPkgDep, { skipReason: 'git-dependency' }),
overrideToPackageDependency(gitOverrideForGithubPkgDep),
bazelModulePackageDepToPackageDependency(gitOverrideForGithubPkgDep),
];
const expectedBazelDepAndSingleVersionOverride: PackageDependency[] = [
deepmerge(bazelDepPkgDep, {
skipReason: 'is-pinned',
registryUrls: [customRegistryUrl],
}),
bazelModulePackageDepToPackageDependency(singleVersionOverridePkgDep),
];
const expectedBazelDepAndArchiveOverride: PackageDependency[] = [
deepmerge(bazelDepPkgDep, { skipReason: 'file-dependency' }),
overrideToPackageDependency(archiveOverridePkgDep),
bazelModulePackageDepToPackageDependency(archiveOverridePkgDep),
];
const expectedBazelDepAndLocalPathOverride: PackageDependency[] = [
deepmerge(bazelDepPkgDep, { skipReason: 'local-dependency' }),
overrideToPackageDependency(localPathOverridePkgDep),
bazelModulePackageDepToPackageDependency(localPathOverridePkgDep),
];
// If a registry is specified and a version is not specified for a
// single_version_override, it is merely providing a registry URL for the bazel_dep.
const expectedBazelDepWithRegistry: PackageDependency[] = [
deepmerge(bazelDepPkgDep, { registryUrls: [customRegistryUrl] }),
];

it.each`
msg | a | exp
${'bazel_dep, no overrides'} | ${[bazelDepPkgDep]} | ${expectedBazelDepNoOverrides}
${'bazel_dep & git_override'} | ${[bazelDepPkgDep, gitOverrideForGithubPkgDep]} | ${expectedBazelDepAndGitOverride}
${'git_override, no bazel_dep'} | ${[gitOverrideForGithubPkgDep]} | ${[]}
${'bazel_dep & archive_override'} | ${[bazelDepPkgDep, archiveOverridePkgDep]} | ${expectedBazelDepAndArchiveOverride}
${'bazel_dep & local_path_override'} | ${[bazelDepPkgDep, localPathOverridePkgDep]} | ${expectedBazelDepAndLocalPathOverride}
`('with $msg', ({ a, exp }) => {
msg | a | exp
${'bazel_dep, no overrides'} | ${[bazelDepPkgDep]} | ${expectedBazelDepNoOverrides}
${'bazel_dep & git_override'} | ${[bazelDepPkgDep, gitOverrideForGithubPkgDep]} | ${expectedBazelDepAndGitOverride}
${'git_override, no bazel_dep'} | ${[gitOverrideForGithubPkgDep]} | ${[]}
${'bazel_dep & archive_override'} | ${[bazelDepPkgDep, archiveOverridePkgDep]} | ${expectedBazelDepAndArchiveOverride}
${'bazel_dep & local_path_override'} | ${[bazelDepPkgDep, localPathOverridePkgDep]} | ${expectedBazelDepAndLocalPathOverride}
${'single_version_override, with version and registry'} | ${[bazelDepPkgDep, singleVersionOverridePkgDep]} | ${expectedBazelDepAndSingleVersionOverride}
${'single_version_override, with registry'} | ${[bazelDepPkgDep, singleVersionOverrideWithRegistryPkgDep]} | ${expectedBazelDepWithRegistry}
${'single_version_override, without version and registry'} | ${[bazelDepPkgDep, singleVersionOverrideWithoutVersionAndRegistryPkgDep]} | ${[bazelDepPkgDep]}
`('with $msg', ({ 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);
Expand Down
82 changes: 73 additions & 9 deletions lib/modules/manager/bazel-module/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,49 @@ export interface BasePackageDep extends PackageDependency {
depName: string;
}

type BasePackageDepMergeKeys = Extract<keyof BasePackageDep, 'registryUrls'>;

export interface MergePackageDep extends BasePackageDep {
// The fields that should be copied from this struct to the bazel_dep
// PackageDependency.
bazelDepMergeFields: BasePackageDepMergeKeys[];
}

export interface OverridePackageDep extends BasePackageDep {
// This value is set as the skipReason on the bazel_dep PackageDependency.
bazelDepSkipReason: SkipReason;
}

export type BazelModulePackageDep = BasePackageDep | OverridePackageDep;
export type BazelModulePackageDep =
| BasePackageDep
| OverridePackageDep
| MergePackageDep;

function isOverride(value: BazelModulePackageDep): value is OverridePackageDep {
return 'bazelDepSkipReason' in value;
}

function isMerge(value: BazelModulePackageDep): value is MergePackageDep {
return 'bazelDepMergeFields' in value;
}

// This function exists to remove properties that are specific to
// OverridePackageDep. In theory, there is no harm in leaving the properties
// BazelModulePackageDep. 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
export function bazelModulePackageDepToPackageDependency(
bmpd: BazelModulePackageDep
): PackageDependency {
const copy: Partial<OverridePackageDep> = { ...override };
delete copy.bazelDepSkipReason;
const copy: BazelModulePackageDep = structuredClone(bmpd);
if (isOverride(copy)) {
const partial = copy as Partial<OverridePackageDep>;
delete partial.bazelDepSkipReason;
}
if (isMerge(copy)) {
const partial = copy as Partial<MergePackageDep>;
delete partial.bazelDepMergeFields;
}
return copy;
}

Expand Down Expand Up @@ -87,6 +109,40 @@ const GitOverrideToPackageDep = RecordFragmentSchema.extend({
}
);

const SingleVersionOverrideToPackageDep = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
value: z.literal('single_version_override'),
}),
module_name: StringFragmentSchema,
version: StringFragmentSchema.optional(),
registry: StringFragmentSchema.optional(),
}),
}).transform(
({
children: { rule, module_name: moduleName, version, registry },
}): BasePackageDep => {
const base: BasePackageDep = {
depType: rule.value,
depName: moduleName.value,
skipReason: 'ignored',
};
// If a version is specified, then add a skipReason to bazel_dep
if (version) {
const override = base as OverridePackageDep;
override.bazelDepSkipReason = 'is-pinned';
override.currentValue = version.value;
}
// If a registry is specified, then merge it into the bazel_dep
if (registry) {
const merge = base as MergePackageDep;
merge.bazelDepMergeFields = ['registryUrls'];
merge.registryUrls = [registry.value];
}
return base;
}
);

const UnsupportedOverrideToPackageDep = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
Expand Down Expand Up @@ -117,6 +173,7 @@ const UnsupportedOverrideToPackageDep = RecordFragmentSchema.extend({
export const RuleToBazelModulePackageDep = z.union([
BazelDepToPackageDep,
GitOverrideToPackageDep,
SingleVersionOverrideToPackageDep,
UnsupportedOverrideToPackageDep,
]);

Expand Down Expand Up @@ -151,7 +208,14 @@ export function processModulePkgDeps(
logger.debug(`A 'bazel_dep' was not found for '${moduleName}'.`);
return [];
}
const deps: PackageDependency[] = [bazelDep];
// Create a new bazelDep that will be modified. We do not want to change the
// input.
const bazelDepOut = { ...bazelDep };
const deps: PackageDependency[] = [bazelDepOut];
const merges = packageDeps.filter(isMerge);
for (const merge of merges) {
merge.bazelDepMergeFields.forEach((k) => (bazelDepOut[k] = merge[k]));
}
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.
Expand All @@ -164,8 +228,8 @@ export function processModulePkgDeps(
return deps;
}
const override = overrides[0];
deps.push(overrideToPackageDependency(override));
bazelDep.skipReason = override.bazelDepSkipReason;
deps.push(bazelModulePackageDepToPackageDependency(override));
bazelDepOut.skipReason = override.bazelDepSkipReason;
return deps;
}

Expand Down