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(manager/helm-values): Add support for bumpVersion #26441

Merged
merged 16 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
}