Skip to content

Commit

Permalink
feat(manager/helm-values): Add support for bumpVersion (#26441)
Browse files Browse the repository at this point in the history
Signed-off-by: kvanzuijlen <8818390+kvanzuijlen@users.noreply.github.com>
  • Loading branch information
kvanzuijlen committed Jan 17, 2024
1 parent 48439d2 commit 3f0eec4
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 14 deletions.
3 changes: 2 additions & 1 deletion docs/usage/configuration-options.md
Expand Up @@ -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`
Expand Down
71 changes: 61 additions & 10 deletions 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',
);
Expand All @@ -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: [
{
Expand All @@ -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: [
{
Expand All @@ -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();
});
});
});
18 changes: 15 additions & 3 deletions lib/modules/manager/helm-values/extract.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -57,10 +58,10 @@ function findDependencies(
return packageDependencies;
}

export function extractPackageFile(
export async function extractPackageFile(
content: string,
packageFile?: string,
): PackageFileContent | null {
packageFile: string,
): Promise<PackageFileContent | null> {
let parsedContent: Record<string, unknown>[] | HelmDockerImageDependency[];
try {
// a parser that allows extracting line numbers would be preferable, with
Expand All @@ -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 */ {
Expand Down
1 change: 1 addition & 0 deletions 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}}',
Expand Down
13 changes: 13 additions & 0 deletions 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<typeof ChartDefinition>;

export const ChartDefinitionYaml = Yaml.pipe(ChartDefinition);
78 changes: 78 additions & 0 deletions 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);
});
});
});
44 changes: 44 additions & 0 deletions 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<BumpPackageVersionResult> {
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,
};
}
43 changes: 43 additions & 0 deletions 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);
Expand Down Expand Up @@ -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<string | null> {
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<ChartDefinition | null> {
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;
}
}

0 comments on commit 3f0eec4

Please sign in to comment.