From c978b4e086b5bd729c76f7c8784430619d67af33 Mon Sep 17 00:00:00 2001 From: Shaun Wilde <sawilde@users.noreply.github.com> Date: Mon, 19 Sep 2022 18:12:52 +1000 Subject: [PATCH] feat(manager/asdf): add support for .tools-versions as used by asdf (#17166) Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> --- lib/modules/manager/api.ts | 2 + lib/modules/manager/asdf/extract.spec.ts | 144 +++++++++++++++++++++++ lib/modules/manager/asdf/extract.ts | 58 +++++++++ lib/modules/manager/asdf/index.ts | 11 ++ lib/modules/manager/asdf/readme.md | 10 ++ 5 files changed, 225 insertions(+) create mode 100644 lib/modules/manager/asdf/extract.spec.ts create mode 100644 lib/modules/manager/asdf/extract.ts create mode 100644 lib/modules/manager/asdf/index.ts create mode 100644 lib/modules/manager/asdf/readme.md diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 0ae05622fc..89d9cc2508 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -1,6 +1,7 @@ import * as ansible from './ansible'; import * as ansibleGalaxy from './ansible-galaxy'; import * as argoCD from './argocd'; +import * as asdf from './asdf'; import * as azurePipelines from './azure-pipelines'; import * as batect from './batect'; import * as batectWrapper from './batect-wrapper'; @@ -83,6 +84,7 @@ export default api; api.set('ansible', ansible); api.set('ansible-galaxy', ansibleGalaxy); api.set('argocd', argoCD); +api.set('asdf', asdf); api.set('azure-pipelines', azurePipelines); api.set('batect', batect); api.set('batect-wrapper', batectWrapper); diff --git a/lib/modules/manager/asdf/extract.spec.ts b/lib/modules/manager/asdf/extract.spec.ts new file mode 100644 index 0000000000..05d0d57152 --- /dev/null +++ b/lib/modules/manager/asdf/extract.spec.ts @@ -0,0 +1,144 @@ +import { extractPackageFile } from '.'; + +describe('modules/manager/asdf/extract', () => { + describe('extractPackageFile()', () => { + it('returns a result', () => { + const res = extractPackageFile('nodejs 16.16.0\n'); + expect(res).toEqual({ + deps: [ + { + currentValue: '16.16.0', + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + }, + ], + }); + }); + + it('provides skipReason for lines with unsupported tooling', () => { + const res = extractPackageFile('unsupported 1.22.5\n'); + expect(res).toEqual({ + deps: [ + { + depName: 'unsupported', + skipReason: 'unsupported-datasource', + }, + ], + }); + }); + + it('only captures the first version', () => { + const res = extractPackageFile('nodejs 16.16.0 16.15.1'); + expect(res).toEqual({ + deps: [ + { + currentValue: '16.16.0', + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + }, + ], + }); + }); + + it('can handle multiple tools in one file', () => { + const res = extractPackageFile('nodejs 16.16.0\ndummy 1.2.3'); + expect(res).toEqual({ + deps: [ + { + currentValue: '16.16.0', + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + }, + { + depName: 'dummy', + skipReason: 'unsupported-datasource', + }, + ], + }); + }); + + describe('comment handling', () => { + const validComments = [ + { + entry: 'nodejs 16.16.0 # tidy comment', + expect: '16.16.0', + }, + { + entry: 'nodejs 16.16.0 #sloppy-comment', + expect: '16.16.0', + }, + ]; + + describe.each(validComments)( + 'ignores proper comments at the end of lines', + (data) => { + it(`entry: '${data.entry}'`, () => { + const res = extractPackageFile(data.entry); + expect(res).toEqual({ + deps: [ + { + currentValue: data.expect, + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + }, + ], + }); + }); + } + ); + + it('invalid comment placements fail to parse', () => { + const res = extractPackageFile( + 'nodejs 16.16.0# invalid comment spacing' + ); + expect(res).toBeNull(); + }); + + it('ignores lines that are just comments', () => { + const res = extractPackageFile('# this is a full line comment\n'); + expect(res).toBeNull(); + }); + + it('ignores comments across multiple lines', () => { + const res = extractPackageFile( + '# this is a full line comment\nnodejs 16.16.0 # this is a comment\n' + ); + expect(res).toEqual({ + deps: [ + { + currentValue: '16.16.0', + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + }, + ], + }); + }); + + it('ignores supported tooling with a renovate:ignore comment', () => { + const res = extractPackageFile('nodejs 16.16.0 # renovate:ignore\n'); + expect(res).toEqual({ + deps: [ + { + currentValue: '16.16.0', + datasource: 'github-tags', + depName: 'node', + packageName: 'nodejs/node', + versioning: 'node', + skipReason: 'ignored', + }, + ], + }); + }); + }); + }); +}); diff --git a/lib/modules/manager/asdf/extract.ts b/lib/modules/manager/asdf/extract.ts new file mode 100644 index 0000000000..2c206c90e3 --- /dev/null +++ b/lib/modules/manager/asdf/extract.ts @@ -0,0 +1,58 @@ +import is from '@sindresorhus/is'; +import { logger } from '../../../logger'; +import { isSkipComment } from '../../../util/ignore'; +import { regEx } from '../../../util/regex'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import * as nodeVersioning from '../../versioning/node'; +import type { PackageDependency, PackageFile } from '../types'; + +const upgradeableTooling: Record< + string, + Pick< + PackageDependency, + 'depName' | 'datasource' | 'packageName' | 'versioning' + > +> = { + nodejs: { + depName: 'node', + datasource: GithubTagsDatasource.id, + packageName: 'nodejs/node', + versioning: nodeVersioning.id, + }, +}; + +export function extractPackageFile(content: string): PackageFile | null { + logger.trace('asdf.extractPackageFile()'); + + const regex = regEx( + /^(?<toolName>(\w+)) (?<version>[^\s#]+)(?: [^\s#]+)* *(?: #(?<comment>.*))?$/gm + ); + + const deps: PackageDependency[] = []; + + for (const groups of [...content.matchAll(regex)] + .map((m) => m.groups) + .filter(is.truthy)) { + const supportedTool = upgradeableTooling[groups.toolName]; + if (supportedTool) { + const dep: PackageDependency = { + currentValue: groups.version.trim(), + ...supportedTool, + }; + if (isSkipComment((groups.comment ?? '').trim())) { + dep.skipReason = 'ignored'; + } + + deps.push(dep); + } else { + const dep: PackageDependency = { + depName: groups.toolName.trim(), + skipReason: 'unsupported-datasource', + }; + + deps.push(dep); + } + } + + return deps.length ? { deps } : null; +} diff --git a/lib/modules/manager/asdf/index.ts b/lib/modules/manager/asdf/index.ts new file mode 100644 index 0000000000..df7dcc8c60 --- /dev/null +++ b/lib/modules/manager/asdf/index.ts @@ -0,0 +1,11 @@ +import { GithubTagsDatasource } from '../../datasource/github-tags'; + +export { extractPackageFile } from './extract'; + +export const displayName = 'asdf'; + +export const defaultConfig = { + fileMatch: ['(^|/)\\.tools-versions$'], +}; + +export const supportedDatasources = [GithubTagsDatasource.id]; diff --git a/lib/modules/manager/asdf/readme.md b/lib/modules/manager/asdf/readme.md new file mode 100644 index 0000000000..f593fcf5ee --- /dev/null +++ b/lib/modules/manager/asdf/readme.md @@ -0,0 +1,10 @@ +Keeps the [asdf](https://asdf-vm.com/manage/configuration.html#tool-versions) +`.tools-versions` file updated. + +Because `asdf` supports the version management of many different tools, specific tool support needs to be added one by one. + +Only the following tools are currently supported + +- [nodejs](https://github.com/asdf-vm/asdf-nodejs) + +NOTE: Because `.tools-versions` can support fallback versions only the first version entry for each supported tool is managed -- GitLab