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(helm-values): Support for bumpVersion #8240

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ This is an advance field and it's recommend you seek a config review before appl

## bumpVersion

Currently this setting supports `helmv3`, `npm` and `sbt` only, so raise a feature request if you have a use for it with other package managers.
Currently this setting supports `helmv3`, `helm-values`, `npm` and `sbt` only, so raise a feature request if you have a use for it with other package managers.
Its purpose is if you want Renovate to update the `version` field within your file's `package.json` any time it updates dependencies within.
Usually this is for automatic release purposes, so that you don't need to add another step after Renovate before you can release a new version.

Expand Down
10 changes: 9 additions & 1 deletion lib/manager/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ export interface UpdateDependencyConfig<T = Record<string, any>> {

export interface BumpPackageVersionResult {
bumpedContent: string | null;
// describes files that was changed instead of or in addition to the packageFile
bumpedFiles?: BumpedPackageFile[];
}

export interface BumpedPackageFile {
fileName: string;
newContent: string;
}

export interface ManagerApi {
Expand All @@ -237,7 +244,8 @@ export interface ManagerApi {
bumpPackageVersion?(
content: string,
currentValue: string,
bumpVersion: ReleaseType | string
bumpVersion: ReleaseType | string,
packageFile?: string
): Result<BumpPackageVersionResult>;

extractAllPackageFiles?(
Expand Down
15 changes: 15 additions & 0 deletions lib/manager/helm-values/__snapshots__/update.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`lib/manager/helm-values/update .bumpPackageVersion() increments 1`] = `
"apiVersion: v2
name: test
version: 0.0.3
"
`;

exports[`lib/manager/helm-values/update .bumpPackageVersion() updates 1`] = `
"apiVersion: v2
name: test
version: 0.1.0
"
`;
60 changes: 50 additions & 10 deletions lib/manager/helm-values/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFileSync } from 'fs';
import { fs } from '../../../test/util';
import { extractPackageFile } from './extract';

const helmDefaultChartInitValues = readFileSync(
Expand All @@ -15,27 +16,66 @@ describe('lib/manager/helm-values/extract', () => {
describe('extractPackageFile()', () => {
beforeEach(() => {
jest.resetAllMocks();
fs.readLocalFile = jest.fn();
});
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: [');
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('');
expect(result).toBeNull();
});
it('returns null for no file content', () => {
const result = extractPackageFile(null);
it('returns null for no file content', async () => {
const result = await extractPackageFile(null);
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);
expect(result).toMatchSnapshot();
});
it('extracts from complex values file correctly"', () => {
const result = extractPackageFile(helmMultiAndNestedImageValues);
it('extracts from complex values file correctly"', async () => {
const result = await extractPackageFile(helmMultiAndNestedImageValues);
expect(result).toMatchSnapshot();
expect(result.deps).toHaveLength(4);
});
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.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();
});
});
});
22 changes: 20 additions & 2 deletions lib/manager/helm-values/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getDep } from '../dockerfile/extract';

import {
HelmDockerImageDependency,
getParsedSiblingChartYaml,
matchesHelmValuesDockerHeuristic,
} from './util';

Expand Down Expand Up @@ -53,7 +54,10 @@ function findDependencies(
return packageDependencies;
}

export function extractPackageFile(content: string): PackageFile {
export async function extractPackageFile(
content: string,
fileName?: string
): Promise<PackageFile> {
let parsedContent: Record<string, unknown> | HelmDockerImageDependency;
try {
// a parser that allows extracting line numbers would be preferable, with
Expand All @@ -68,7 +72,21 @@ export function extractPackageFile(content: string): PackageFile {
const deps = findDependencies(parsedContent, []);
if (deps.length) {
logger.debug({ deps }, 'Found dependencies in helm-values');
return { deps };

// 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(fileName);
const packageFileVersion = siblingChart?.version;
if (packageFileVersion) {
return {
deps,
packageFileVersion,
};
}
return {
deps,
};
}
} catch (err) /* istanbul ignore next */ {
logger.error({ err }, 'Error parsing helm-values parsed content');
Expand Down
1 change: 1 addition & 0 deletions lib/manager/helm-values/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { extractPackageFile } from './extract';
export { bumpPackageVersion } from './update';

export const defaultConfig = {
commitMessageTopic: 'helm values {{depName}}',
Expand Down
85 changes: 85 additions & 0 deletions lib/manager/helm-values/update.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import yaml from 'js-yaml';
import { fs } from '../../../test/util';
import * as helmValuesUpdater from './update';

describe('lib/manager/helm-values/update', () => {
describe('.bumpPackageVersion()', () => {
const chartContent = yaml.safeDump({
apiVersion: 'v2',
name: 'test',
version: '0.0.2',
});
const helmValuesContent = yaml.safeDump({
image: {
registry: 'docker.io',
repository: 'docker/whalesay',
tag: '1.0.0',
},
});
beforeEach(() => {
jest.resetAllMocks();
fs.readLocalFile = jest.fn();
fs.readLocalFile.mockResolvedValueOnce(chartContent);
});
it('increments', async () => {
const {
bumpedContent,
bumpedFiles,
} = await helmValuesUpdater.bumpPackageVersion(
helmValuesContent,
'0.0.2',
'patch',
'values.yaml'
);
expect(bumpedContent).toEqual(helmValuesContent);
expect(bumpedFiles).toHaveLength(1);
const bumpedFile = bumpedFiles[0];
expect(bumpedFile.fileName).toEqual('Chart.yaml');
expect(bumpedFile.newContent).toMatchSnapshot();
});
it('no ops', async () => {
const {
bumpedContent,
bumpedFiles,
} = await helmValuesUpdater.bumpPackageVersion(
helmValuesContent,
'0.0.1',
'patch',
'values.yaml'
);
expect(bumpedContent).toEqual(helmValuesContent);
expect(bumpedFiles).toHaveLength(1);
const bumpedFile = bumpedFiles[0];
expect(bumpedFile.newContent).toEqual(chartContent);
});
it('updates', async () => {
const {
bumpedContent,
bumpedFiles,
} = await helmValuesUpdater.bumpPackageVersion(
helmValuesContent,
'0.0.1',
'minor',
'values.yaml'
);
expect(bumpedContent).toEqual(helmValuesContent);
expect(bumpedFiles).toHaveLength(1);
const bumpedFile = bumpedFiles[0];
expect(bumpedFile.fileName).toEqual('Chart.yaml');
expect(bumpedFile.newContent).toMatchSnapshot();
});
it('returns content if bumping errors', async () => {
const {
bumpedContent,
bumpedFiles,
} = await helmValuesUpdater.bumpPackageVersion(
helmValuesContent,
'0.0.2',
true as any,
'values.yaml'
);
expect(bumpedContent).toEqual(helmValuesContent);
expect(bumpedFiles).toBeUndefined();
});
});
});
51 changes: 51 additions & 0 deletions lib/manager/helm-values/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ReleaseType, inc } from 'semver';
import { logger } from '../../logger';
import { getSiblingFileName } from '../../util/fs';
import { BumpPackageVersionResult } from '../common';
import { getSiblingChartYamlContent } from './util';

export async function bumpPackageVersion(
content: string,
currentValue: string,
bumpVersion: ReleaseType | string,
packageFile: string
): Promise<BumpPackageVersionResult> {
logger.debug(
{ bumpVersion, currentValue },
'Checking if we should bump Chart.yaml version'
);
const chartFileName = getSiblingFileName(packageFile, 'Chart.yaml');
const chartYamlContent = await getSiblingChartYamlContent(packageFile);
try {
const newChartVersion = inc(currentValue, bumpVersion as ReleaseType);
if (!newChartVersion) {
throw new Error('semver inc failed');
}
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,
bumpedFiles: [{ fileName: chartFileName, newContent: bumpedContent }],
};
} catch (err) {
logger.warn(
{
chartYamlContent,
currentValue,
bumpVersion,
},
'Failed to bumpVersion'
);
return {
bumpedContent: content,
};
}
}
52 changes: 52 additions & 0 deletions lib/manager/helm-values/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import yaml from 'js-yaml';
import { logger } from '../../logger';
import { getSiblingFileName, readLocalFile } from '../../util/fs';
import { hasKey } from '../../util/object';

export type HelmDockerImageDependency = {
Expand Down Expand Up @@ -35,3 +38,52 @@ export function matchesHelmValuesDockerHeuristic(
hasKey('tag', data)
);
}

/**
* 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> {
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<any> {
try {
const chartContents = await getSiblingChartYamlContent(fileName);
if (!chartContents) {
logger.debug({ fileName }, 'Failed to find helm Chart.yaml');
return null;
}
// TODO: fix me
const chart = yaml.safeLoad(chartContents, { json: true }) as any;
if (!(chart?.apiVersion && chart.name && chart.version)) {
logger.debug(
{ fileName },
'Failed to find required fields in Chart.yaml'
);
return null;
}
return chart;
} catch (err) {
logger.debug({ fileName }, 'Failed to parse helm Chart.yaml');
return null;
}
}
Loading