diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index bb4a74e298a9ad..88e5b2c9d0bbe7 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -406,13 +406,14 @@ Instead, set the old `branchPrefix` value as `branchPrefixOld` to allow Renovate ## branchTopic This field is combined with `branchPrefix` and `additionalBranchPrefix` to form the full `branchName`. `branchName` uniqueness is important for dependency update grouping or non-grouping so be cautious about ever editing this field manually. -This is an advance field and it's recommend you seek a config review before applying it. +This is an advanced field, and it's recommend you seek a config review before applying it. ## bumpVersion Currently, this config option only works with these managers: - `helmv3` +- `helm-values` - `npm` - `nuget` - `maven` diff --git a/lib/modules/manager/helm-values/extract.spec.ts b/lib/modules/manager/helm-values/extract.spec.ts index d8eb805db86f7a..f9397c655de745 100644 --- a/lib/modules/manager/helm-values/extract.spec.ts +++ b/lib/modules/manager/helm-values/extract.spec.ts @@ -1,6 +1,9 @@ import { Fixtures } from '../../../../test/fixtures'; +import { fs } from '../../../../test/util'; import { extractPackageFile } from '.'; +jest.mock('../../../util/fs'); + const helmDefaultChartInitValues = Fixtures.get( 'default_chart_init_values.yaml', ); @@ -11,18 +14,21 @@ const helmMultiAndNestedImageValues = Fixtures.get( describe('modules/manager/helm-values/extract', () => { describe('extractPackageFile()', () => { - it('returns null for invalid yaml file content', () => { - const result = extractPackageFile('nothing here: ['); + it('returns null for invalid yaml file content', async () => { + const result = await extractPackageFile('nothing here: [', 'some file'); expect(result).toBeNull(); }); - it('returns null for empty yaml file content', () => { - const result = extractPackageFile(''); + it('returns null for empty yaml file content', async () => { + const result = await extractPackageFile('', 'some file'); expect(result).toBeNull(); }); - it('extracts from values.yaml correctly with same structure as "helm create"', () => { - const result = extractPackageFile(helmDefaultChartInitValues); + it('extracts from values.yaml correctly with same structure as "helm create"', async () => { + const result = await extractPackageFile( + helmDefaultChartInitValues, + 'some file', + ); expect(result).toMatchSnapshot({ deps: [ { @@ -33,17 +39,20 @@ describe('modules/manager/helm-values/extract', () => { }); }); - it('extracts from complex values file correctly"', () => { - const result = extractPackageFile(helmMultiAndNestedImageValues); + it('extracts from complex values file correctly"', async () => { + const result = await extractPackageFile( + helmMultiAndNestedImageValues, + 'some file', + ); expect(result).toMatchSnapshot(); expect(result?.deps).toHaveLength(5); }); - it('extract data from file with multiple documents', () => { + it('extract data from file with multiple documents', async () => { const multiDocumentFile = Fixtures.get( 'single_file_with_multiple_documents.yaml', ); - const result = extractPackageFile(multiDocumentFile); + const result = await extractPackageFile(multiDocumentFile, 'some file'); expect(result).toMatchObject({ deps: [ { @@ -61,5 +70,47 @@ describe('modules/manager/helm-values/extract', () => { ], }); }); + + it('returns the package file version from the sibling Chart.yaml"', async () => { + fs.readLocalFile.mockResolvedValueOnce(` + apiVersion: v2 + appVersion: "1.0" + description: A Helm chart for Kubernetes + name: example + version: 0.1.0 + `); + const result = await extractPackageFile( + helmMultiAndNestedImageValues, + 'values.yaml', + ); + expect(result).not.toBeNull(); + expect(result?.packageFileVersion).toBe('0.1.0'); + }); + + it('does not fail if the sibling Chart.yaml is invalid', async () => { + fs.readLocalFile.mockResolvedValueOnce(` + invalidYaml: [ + `); + const result = await extractPackageFile( + helmMultiAndNestedImageValues, + 'values.yaml', + ); + expect(result).not.toBeNull(); + expect(result?.packageFileVersion).toBeUndefined(); + }); + + it('does not fail if the sibling Chart.yaml does not contain the required fields', async () => { + fs.readLocalFile.mockResolvedValueOnce(` + apiVersion: v2 + name: test + version-is: missing + `); + const result = await extractPackageFile( + helmMultiAndNestedImageValues, + 'values.yaml', + ); + expect(result).not.toBeNull(); + expect(result?.packageFileVersion).toBeUndefined(); + }); }); }); diff --git a/lib/modules/manager/helm-values/extract.ts b/lib/modules/manager/helm-values/extract.ts index 61437fdbb79571..7e707aab22327b 100644 --- a/lib/modules/manager/helm-values/extract.ts +++ b/lib/modules/manager/helm-values/extract.ts @@ -5,6 +5,7 @@ import { getDep } from '../dockerfile/extract'; import type { PackageDependency, PackageFileContent } from '../types'; import type { HelmDockerImageDependency } from './types'; import { + getParsedSiblingChartYaml, matchesHelmValuesDockerHeuristic, matchesHelmValuesInlineImage, } from './util'; @@ -57,10 +58,10 @@ function findDependencies( return packageDependencies; } -export function extractPackageFile( +export async function extractPackageFile( content: string, - packageFile?: string, -): PackageFileContent | null { + packageFile: string, +): Promise { let parsedContent: Record[] | HelmDockerImageDependency[]; try { // a parser that allows extracting line numbers would be preferable, with @@ -79,6 +80,17 @@ export function extractPackageFile( } if (deps.length) { + // in Helm, the current package version is the version of the chart. + // This fetches this version by reading it from the Chart.yaml + // found in the same folder as the currently processed values file. + const siblingChart = await getParsedSiblingChartYaml(packageFile); + const packageFileVersion = siblingChart?.version; + if (packageFileVersion) { + return { + deps, + packageFileVersion, + }; + } return { deps }; } } catch (err) /* istanbul ignore next */ { diff --git a/lib/modules/manager/helm-values/index.ts b/lib/modules/manager/helm-values/index.ts index 6d96f591c1b390..61290829cab14b 100644 --- a/lib/modules/manager/helm-values/index.ts +++ b/lib/modules/manager/helm-values/index.ts @@ -1,6 +1,7 @@ import type { Category } from '../../../constants'; import { DockerDatasource } from '../../datasource/docker'; export { extractPackageFile } from './extract'; +export { bumpPackageVersion } from './update'; export const defaultConfig = { commitMessageTopic: 'helm values {{depName}}', diff --git a/lib/modules/manager/helm-values/schema.ts b/lib/modules/manager/helm-values/schema.ts new file mode 100644 index 00000000000000..c7f77fe7756323 --- /dev/null +++ b/lib/modules/manager/helm-values/schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; +import { Yaml } from '../../../util/schema-utils'; + +export const ChartDefinition = z + .object({ + apiVersion: z.string().regex(/v([12])/), + name: z.string().min(1), + version: z.string().min(1), + }) + .partial(); +export type ChartDefinition = z.infer; + +export const ChartDefinitionYaml = Yaml.pipe(ChartDefinition); diff --git a/lib/modules/manager/helm-values/update.spec.ts b/lib/modules/manager/helm-values/update.spec.ts new file mode 100644 index 00000000000000..10290fb3766070 --- /dev/null +++ b/lib/modules/manager/helm-values/update.spec.ts @@ -0,0 +1,78 @@ +import yaml from 'js-yaml'; +import { fs } from '../../../../test/util'; +import * as helmValuesUpdater from './update'; + +jest.mock('../../../util/fs'); + +describe('modules/manager/helm-values/update', () => { + describe('.bumpPackageVersion()', () => { + const chartContent = yaml.dump({ + apiVersion: 'v2', + name: 'test', + version: '0.0.2', + }); + const helmValuesContent = yaml.dump({ + image: { + registry: 'docker.io', + repository: 'docker/whalesay', + tag: '1.0.0', + }, + }); + + beforeEach(() => { + fs.readLocalFile.mockResolvedValueOnce(chartContent); + }); + + it('increments', async () => { + const { bumpedContent } = await helmValuesUpdater.bumpPackageVersion( + helmValuesContent, + '0.0.2', + 'patch', + 'test/values.yaml', + ); + expect(bumpedContent).toEqual(helmValuesContent); + }); + + it('no ops', async () => { + const { bumpedContent } = await helmValuesUpdater.bumpPackageVersion( + helmValuesContent, + '0.0.1', + 'patch', + 'values.yaml', + ); + expect(bumpedContent).toEqual(helmValuesContent); + }); + + it('updates', async () => { + const { bumpedContent } = await helmValuesUpdater.bumpPackageVersion( + helmValuesContent, + '0.0.1', + 'minor', + 'test/values.yaml', + ); + expect(bumpedContent).toEqual(helmValuesContent); + }); + + it('returns content if bumping errors', async () => { + const { bumpedContent } = await helmValuesUpdater.bumpPackageVersion( + helmValuesContent, + '0.0.2', + true as any, + 'values.yaml', + ); + expect(bumpedContent).toEqual(helmValuesContent); + }); + + it('returns content if retrieving Chart.yaml fails', async () => { + fs.readLocalFile.mockReset(); + fs.readLocalFile.mockRejectedValueOnce(null); + const { bumpedContent } = await helmValuesUpdater.bumpPackageVersion( + helmValuesContent, + '0.0.2', + 'minor', + 'values.yaml', + ); + expect(bumpedContent).toEqual(helmValuesContent); + }); + }); +}); diff --git a/lib/modules/manager/helm-values/update.ts b/lib/modules/manager/helm-values/update.ts new file mode 100644 index 00000000000000..1fb9e9a9fec3cf --- /dev/null +++ b/lib/modules/manager/helm-values/update.ts @@ -0,0 +1,44 @@ +import { ReleaseType, inc } from 'semver'; +import { logger } from '../../../logger'; +import type { BumpPackageVersionResult } from '../types'; +import { getSiblingChartYamlContent } from './util'; + +export async function bumpPackageVersion( + content: string, + currentValue: string, + bumpVersion: ReleaseType, + packageFile: string, +): Promise { + logger.debug( + { bumpVersion, currentValue }, + 'Checking if we should bump Chart.yaml version', + ); + const chartYamlContent = await getSiblingChartYamlContent(packageFile); + const newChartVersion = inc(currentValue, bumpVersion); + if (!newChartVersion || chartYamlContent === null) { + logger.warn( + { + chartYamlContent, + currentValue, + bumpVersion, + }, + 'Failed to bumpVersion', + ); + return { + bumpedContent: content, + }; + } + logger.debug({ newChartVersion }); + const bumpedContent = chartYamlContent?.replace( + /^(version:\s*).*$/m, + `$1${newChartVersion}`, + ); + if (bumpedContent === chartYamlContent) { + logger.debug('Version was already bumped'); + } else { + logger.debug('Bumped Chart.yaml version'); + } + return { + bumpedContent: content, + }; +} diff --git a/lib/modules/manager/helm-values/util.ts b/lib/modules/manager/helm-values/util.ts index 34b6c2dabc86ab..26814c6f46e794 100644 --- a/lib/modules/manager/helm-values/util.ts +++ b/lib/modules/manager/helm-values/util.ts @@ -1,5 +1,8 @@ +import { logger } from '../../../logger'; +import { getSiblingFileName, readLocalFile } from '../../../util/fs'; import { hasKey } from '../../../util/object'; import { regEx } from '../../../util/regex'; +import { type ChartDefinition, ChartDefinitionYaml } from './schema'; import type { HelmDockerImageDependency } from './types'; const parentKeyRe = regEx(/image$/i); @@ -41,3 +44,43 @@ export function matchesHelmValuesInlineImage( ): data is string { return !!(parentKeyRe.test(parentKey) && data && typeof data === 'string'); } + +/** + * This function looks for a Chart.yaml in the same directory as @param fileName and + * returns its raw contents. + * + * @param fileName + */ +export async function getSiblingChartYamlContent( + fileName: string, +): Promise { + try { + const chartFileName = getSiblingFileName(fileName, 'Chart.yaml'); + return await readLocalFile(chartFileName, 'utf8'); + } catch (err) { + logger.debug({ fileName }, 'Failed to read helm Chart.yaml'); + return null; + } +} + +/** + * This function looks for a Chart.yaml in the same directory as @param fileName and + * if it looks like a valid Helm Chart.yaml, it is parsed and returned as an object. + * + * @param fileName + */ +export async function getParsedSiblingChartYaml( + fileName: string, +): Promise { + try { + const chartContents = await getSiblingChartYamlContent(fileName); + if (!chartContents) { + logger.debug({ fileName }, 'Failed to find helm Chart.yaml'); + return null; + } + return ChartDefinitionYaml.parse(chartContents); + } catch (err) { + logger.debug({ fileName }, 'Failed to parse helm Chart.yaml'); + return null; + } +}