diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index caedf1edc0e98d..6fe22b70477898 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -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. diff --git a/lib/manager/common.ts b/lib/manager/common.ts index 8896d5bf1aca93..7795b4e7bcddbb 100644 --- a/lib/manager/common.ts +++ b/lib/manager/common.ts @@ -227,6 +227,13 @@ export interface UpdateDependencyConfig> { 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 { @@ -237,7 +244,8 @@ export interface ManagerApi { bumpPackageVersion?( content: string, currentValue: string, - bumpVersion: ReleaseType | string + bumpVersion: ReleaseType | string, + packageFile?: string ): Result; extractAllPackageFiles?( diff --git a/lib/manager/helm-values/__snapshots__/update.spec.ts.snap b/lib/manager/helm-values/__snapshots__/update.spec.ts.snap new file mode 100644 index 00000000000000..e1be96d3a072eb --- /dev/null +++ b/lib/manager/helm-values/__snapshots__/update.spec.ts.snap @@ -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 +" +`; diff --git a/lib/manager/helm-values/extract.spec.ts b/lib/manager/helm-values/extract.spec.ts index 417c3825f5770c..a78458e0f4413c 100644 --- a/lib/manager/helm-values/extract.spec.ts +++ b/lib/manager/helm-values/extract.spec.ts @@ -1,4 +1,5 @@ import { readFileSync } from 'fs'; +import { fs } from '../../../test/util'; import { extractPackageFile } from './extract'; const helmDefaultChartInitValues = readFileSync( @@ -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(); + }); }); }); diff --git a/lib/manager/helm-values/extract.ts b/lib/manager/helm-values/extract.ts index da5b127a65bd02..e35cb54b10476b 100644 --- a/lib/manager/helm-values/extract.ts +++ b/lib/manager/helm-values/extract.ts @@ -6,6 +6,7 @@ import { getDep } from '../dockerfile/extract'; import { HelmDockerImageDependency, + getParsedSiblingChartYaml, matchesHelmValuesDockerHeuristic, } from './util'; @@ -53,7 +54,10 @@ function findDependencies( return packageDependencies; } -export function extractPackageFile(content: string): PackageFile { +export async function extractPackageFile( + content: string, + fileName?: string +): Promise { let parsedContent: Record | HelmDockerImageDependency; try { // a parser that allows extracting line numbers would be preferable, with @@ -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'); diff --git a/lib/manager/helm-values/index.ts b/lib/manager/helm-values/index.ts index 1ef75d28735eca..11d8f2279aefae 100644 --- a/lib/manager/helm-values/index.ts +++ b/lib/manager/helm-values/index.ts @@ -1,4 +1,5 @@ export { extractPackageFile } from './extract'; +export { bumpPackageVersion } from './update'; export const defaultConfig = { commitMessageTopic: 'helm values {{depName}}', diff --git a/lib/manager/helm-values/update.spec.ts b/lib/manager/helm-values/update.spec.ts new file mode 100644 index 00000000000000..e90a73547e9f42 --- /dev/null +++ b/lib/manager/helm-values/update.spec.ts @@ -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(); + }); + }); +}); diff --git a/lib/manager/helm-values/update.ts b/lib/manager/helm-values/update.ts new file mode 100644 index 00000000000000..25184d07fca5fc --- /dev/null +++ b/lib/manager/helm-values/update.ts @@ -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 { + 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, + }; + } +} diff --git a/lib/manager/helm-values/util.ts b/lib/manager/helm-values/util.ts index 1aeabaf30b37f4..cc9a21f7a7bd11 100644 --- a/lib/manager/helm-values/util.ts +++ b/lib/manager/helm-values/util.ts @@ -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 = { @@ -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 { + 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; + } + // 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; + } +} diff --git a/lib/workers/branch/__snapshots__/get-updated.spec.ts.snap b/lib/workers/branch/__snapshots__/get-updated.spec.ts.snap index 84acb475c96cb7..f6fdd541c1ac6b 100644 --- a/lib/workers/branch/__snapshots__/get-updated.spec.ts.snap +++ b/lib/workers/branch/__snapshots__/get-updated.spec.ts.snap @@ -1,19 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`workers/branch/get-updated getUpdatedPackageFiles() bumps versions in autoReplace managers 1`] = ` -Object { - "artifactErrors": Array [], - "reuseExistingBranch": undefined, - "updatedArtifacts": Array [], - "updatedPackageFiles": Array [ - Object { - "contents": "version: 0.0.2", - "name": "undefined", - }, - ], -} -`; - exports[`workers/branch/get-updated getUpdatedPackageFiles() bumps versions in updateDependency managers 1`] = ` Object { "artifactErrors": Array [], @@ -186,3 +172,49 @@ Object { ], } `; + +exports[`workers/branch/get-updated getUpdatedPackageFiles() in autoReplace managers bumps versions 1`] = ` +Object { + "artifactErrors": Array [], + "reuseExistingBranch": undefined, + "updatedArtifacts": Array [], + "updatedPackageFiles": Array [ + Object { + "contents": "version: 0.0.2", + "name": "undefined", + }, + ], +} +`; + +exports[`workers/branch/get-updated getUpdatedPackageFiles() in autoReplace managers bumps versions in all files if multiple files were bumped 1`] = ` +Object { + "artifactErrors": Array [], + "reuseExistingBranch": undefined, + "updatedArtifacts": Array [], + "updatedPackageFiles": Array [ + Object { + "contents": "version: 0.0.2", + "name": "/test/Chart.yaml", + }, + Object { + "contents": "# Version 0.0.2", + "name": "/test/README.md", + }, + ], +} +`; + +exports[`workers/branch/get-updated getUpdatedPackageFiles() in autoReplace managers bumps versions with a bumpPackageFile different from the packageFile 1`] = ` +Object { + "artifactErrors": Array [], + "reuseExistingBranch": undefined, + "updatedArtifacts": Array [], + "updatedPackageFiles": Array [ + Object { + "contents": "version: 0.0.2", + "name": "/test/Chart.yaml", + }, + ], +} +`; diff --git a/lib/workers/branch/get-updated.spec.ts b/lib/workers/branch/get-updated.spec.ts index dd260a02b19e24..a28f47fcd285bf 100644 --- a/lib/workers/branch/get-updated.spec.ts +++ b/lib/workers/branch/get-updated.spec.ts @@ -2,6 +2,7 @@ import { defaultConfig, git, mocked } from '../../../test/util'; import * as datasourceGitSubmodules from '../../datasource/git-submodules'; import * as _composer from '../../manager/composer'; import * as _gitSubmodules from '../../manager/git-submodules'; +import * as _helmValues from '../../manager/helm-values'; import * as _helmv3 from '../../manager/helmv3'; import * as _npm from '../../manager/npm'; import { BranchConfig } from '../common'; @@ -11,11 +12,13 @@ import { getUpdatedPackageFiles } from './get-updated'; const composer = mocked(_composer); const gitSubmodules = mocked(_gitSubmodules); const helmv3 = mocked(_helmv3); +const helmValues = mocked(_helmValues); const npm = mocked(_npm); const autoReplace = mocked(_autoReplace); jest.mock('../../manager/composer'); jest.mock('../../manager/helmv3'); +jest.mock('../../manager/helm-values'); jest.mock('../../manager/npm'); jest.mock('../../manager/git-submodules'); jest.mock('../../util/git'); @@ -183,18 +186,63 @@ describe('workers/branch/get-updated', () => { const res = await getUpdatedPackageFiles(config); expect(res).toMatchSnapshot(); }); - it('bumps versions in autoReplace managers', async () => { - config.upgrades.push({ - branchName: undefined, - bumpVersion: 'patch', - manager: 'helmv3', + + describe('in autoReplace managers', () => { + it('bumps versions', async () => { + config.upgrades.push({ + branchName: undefined, + bumpVersion: 'patch', + manager: 'helmv3', + }); + autoReplace.doAutoReplace.mockResolvedValueOnce('version: 0.0.1'); + helmv3.bumpPackageVersion.mockReturnValue({ + bumpedContent: 'version: 0.0.2', + }); + const res = await getUpdatedPackageFiles(config); + expect(res).toMatchSnapshot(); }); - autoReplace.doAutoReplace.mockResolvedValueOnce('version: 0.0.1'); - helmv3.bumpPackageVersion.mockReturnValue({ - bumpedContent: 'version: 0.0.2', + it('bumps versions with a bumpPackageFile different from the packageFile', async () => { + config.upgrades.push({ + branchName: undefined, + bumpVersion: 'patch', + manager: 'helm-values', + }); + autoReplace.doAutoReplace.mockResolvedValueOnce('existing content'); + helmValues.bumpPackageVersion.mockResolvedValue({ + bumpedContent: 'existing content', + bumpedFiles: [ + { + fileName: '/test/Chart.yaml', + newContent: 'version: 0.0.2', + }, + ], + }); + const res = await getUpdatedPackageFiles(config); + expect(res).toMatchSnapshot(); + }); + it('bumps versions in all files if multiple files were bumped', async () => { + config.upgrades.push({ + branchName: undefined, + bumpVersion: 'patch', + manager: 'helm-values', + }); + autoReplace.doAutoReplace.mockResolvedValueOnce('existing content'); + helmValues.bumpPackageVersion.mockResolvedValue({ + bumpedContent: 'existing content', + bumpedFiles: [ + { + fileName: '/test/Chart.yaml', + newContent: 'version: 0.0.2', + }, + { + fileName: '/test/README.md', + newContent: '# Version 0.0.2', + }, + ], + }); + const res = await getUpdatedPackageFiles(config); + expect(res).toMatchSnapshot(); }); - const res = await getUpdatedPackageFiles(config); - expect(res).toMatchSnapshot(); }); }); }); diff --git a/lib/workers/branch/get-updated.ts b/lib/workers/branch/get-updated.ts index 8fd9a83b6117a2..c23ca1e227b430 100644 --- a/lib/workers/branch/get-updated.ts +++ b/lib/workers/branch/get-updated.ts @@ -3,7 +3,7 @@ import { WORKER_FILE_UPDATE_FAILED } from '../../constants/error-messages'; import * as datasourceGitSubmodules from '../../datasource/git-submodules'; import { logger } from '../../logger'; import { get } from '../../manager'; -import { ArtifactError } from '../../manager/common'; +import { ArtifactError, BumpedPackageFile } from '../../manager/common'; import { File, getFile } from '../../util/git'; import { BranchConfig } from '../common'; import { doAutoReplace } from './auto-replace'; @@ -63,14 +63,18 @@ export async function getUpdatedPackageFiles( packageFileContent, reuseExistingBranch ); + if (res) { + let bumpedPackageFiles: BumpedPackageFile[]; if (bumpPackageVersion && upgrade.bumpVersion) { - const { bumpedContent } = await bumpPackageVersion( + const bumpResult = await bumpPackageVersion( res, upgrade.packageFileVersion, - upgrade.bumpVersion + upgrade.bumpVersion, + packageFile ); - res = bumpedContent; + res = bumpResult.bumpedContent; + bumpedPackageFiles = bumpResult.bumpedFiles; } if (res === packageFileContent) { logger.debug({ packageFile, depName }, 'No content changed'); @@ -82,6 +86,18 @@ export async function getUpdatedPackageFiles( logger.debug({ packageFile, depName }, 'Contents updated'); updatedFileContents[packageFile] = res; } + // indicates that the version was bumped in one or more files in + // addition to or instead of the packageFile + if (bumpedPackageFiles) { + for (const bumpedPackageFile of bumpedPackageFiles) { + logger.debug( + { bumpedPackageFile, depName }, + 'Updating bumpedPackageFile content' + ); + updatedFileContents[bumpedPackageFile.fileName] = + bumpedPackageFile.newContent; + } + } continue; // eslint-disable-line no-continue } else if (reuseExistingBranch) { return getUpdatedPackageFiles({ @@ -100,7 +116,8 @@ export async function getUpdatedPackageFiles( const { bumpedContent } = await bumpPackageVersion( newContent, upgrade.packageFileVersion, - upgrade.bumpVersion + upgrade.bumpVersion, + packageFile ); newContent = bumpedContent; }