diff --git a/lib/modules/manager/mise/backends.spec.ts b/lib/modules/manager/mise/backends.spec.ts new file mode 100644 index 00000000000000..508addca3b338d --- /dev/null +++ b/lib/modules/manager/mise/backends.spec.ts @@ -0,0 +1,263 @@ +import { + createAquaToolConfig, + createCargoToolConfig, + createDotnetToolConfig, + createGemToolConfig, + createGoToolConfig, + createNpmToolConfig, + createPipxToolConfig, + createSpmToolConfig, + createUbiToolConfig, +} from './backends'; + +describe('modules/manager/mise/backends', () => { + describe('createAquaToolConfig()', () => { + it('should create a tooling config', () => { + expect( + createAquaToolConfig('BurntSushi/ripgrep', '14.1.1'), + ).toStrictEqual({ + packageName: 'BurntSushi/ripgrep', + datasource: 'github-tags', + currentValue: '14.1.1', + extractVersion: '^v?(?.+)', + }); + }); + + it('should trim the leading v from version', () => { + expect( + createAquaToolConfig('BurntSushi/ripgrep', 'v14.1.1'), + ).toStrictEqual({ + packageName: 'BurntSushi/ripgrep', + datasource: 'github-tags', + currentValue: '14.1.1', + extractVersion: '^v?(?.+)', + }); + }); + }); + + describe('createCargoToolConfig()', () => { + it('should create a tooling config for crate', () => { + expect(createCargoToolConfig('eza', '')).toStrictEqual({ + packageName: 'eza', + datasource: 'crate', + }); + }); + + it('should create a tooling config for git tag', () => { + expect( + createCargoToolConfig('https://github.com/username/demo', 'tag:v0.1.0'), + ).toStrictEqual({ + packageName: 'https://github.com/username/demo', + currentValue: 'v0.1.0', + datasource: 'git-tags', + }); + }); + + it('should provide skipReason for git branch', () => { + expect( + createCargoToolConfig( + 'https://github.com/username/demo', + 'branch:main', + ), + ).toStrictEqual({ + packageName: 'https://github.com/username/demo', + skipReason: 'unsupported-version', + }); + }); + + it('should create a tooling config for git rev', () => { + expect( + createCargoToolConfig('https://github.com/username/demo', 'rev:abcdef'), + ).toStrictEqual({ + packageName: 'https://github.com/username/demo', + currentValue: 'abcdef', + datasource: 'git-refs', + }); + }); + + it('should provide skipReason for invalid version', () => { + expect( + createCargoToolConfig('https://github.com/username/demo', 'v0.1.0'), + ).toStrictEqual({ + packageName: 'https://github.com/username/demo', + skipReason: 'invalid-version', + }); + }); + }); + + describe('createDotnetToolConfig()', () => { + it('should create a tooling config', () => { + expect(createDotnetToolConfig('GitVersion.Tool')).toStrictEqual({ + packageName: 'GitVersion.Tool', + datasource: 'nuget', + }); + }); + }); + + describe('createGemToolConfig()', () => { + it('should create a tooling config', () => { + expect(createGemToolConfig('rubocop')).toStrictEqual({ + packageName: 'rubocop', + datasource: 'rubygems', + }); + }); + }); + + describe('createGoToolConfig()', () => { + it('should create a tooling config', () => { + expect(createGoToolConfig('github.com/DarthSim/hivemind')).toStrictEqual({ + packageName: 'github.com/DarthSim/hivemind', + datasource: 'go', + }); + }); + }); + + describe('createNpmToolConfig()', () => { + it('should create a tooling config', () => { + expect(createNpmToolConfig('prettier')).toStrictEqual({ + packageName: 'prettier', + datasource: 'npm', + }); + }); + }); + + describe('createPipxToolConfig()', () => { + it('should create a tooling config for pypi package', () => { + expect(createPipxToolConfig('yamllint')).toStrictEqual({ + packageName: 'yamllint', + datasource: 'pypi', + }); + }); + + it('should create a tooling config for github shorthand', () => { + expect(createPipxToolConfig('psf/black')).toStrictEqual({ + packageName: 'psf/black', + datasource: 'github-tags', + }); + }); + + it('should create a tooling config for github url', () => { + expect( + createPipxToolConfig('git+https://github.com/psf/black.git'), + ).toStrictEqual({ + packageName: 'psf/black', + datasource: 'github-tags', + }); + }); + + it('should create a tooling config for git url', () => { + expect( + createPipxToolConfig('git+https://gitlab.com/user/repo.git'), + ).toStrictEqual({ + packageName: 'https://gitlab.com/user/repo', + datasource: 'git-refs', + }); + }); + + it('provides skipReason for zip file url', () => { + expect( + createPipxToolConfig('https://github.com/psf/black/archive/18.9b0.zip'), + ).toStrictEqual({ + packageName: 'https://github.com/psf/black/archive/18.9b0.zip', + skipReason: 'unsupported-url', + }); + }); + }); + + describe('createSpmToolConfig()', () => { + it('should create a tooling config for github shorthand', () => { + expect(createSpmToolConfig('tuist/tuist')).toStrictEqual({ + packageName: 'tuist/tuist', + datasource: 'github-releases', + }); + }); + + it('should create a tooling config for github url', () => { + expect( + createSpmToolConfig('https://github.com/tuist/tuist.git'), + ).toStrictEqual({ + packageName: 'tuist/tuist', + datasource: 'github-releases', + }); + }); + + it('provides skipReason for other url', () => { + expect( + createSpmToolConfig('https://gitlab.com/user/repo.git'), + ).toStrictEqual({ + packageName: 'https://gitlab.com/user/repo.git', + skipReason: 'unsupported-url', + }); + }); + }); + + describe('createUbiToolConfig()', () => { + it('should create a tooling config with empty options', () => { + expect(createUbiToolConfig('nekto/act', '0.2.70', {})).toStrictEqual({ + packageName: 'nekto/act', + datasource: 'github-releases', + currentValue: '0.2.70', + extractVersion: '^v?(?.+)', + }); + }); + + it('should trim the leading v from version', () => { + expect(createUbiToolConfig('cli/cli', 'v2.64.0', {})).toStrictEqual({ + packageName: 'cli/cli', + datasource: 'github-releases', + currentValue: '2.64.0', + extractVersion: '^v?(?.+)', + }); + }); + + it('should ignore options unless tag_regex is provided', () => { + expect( + createUbiToolConfig('cli/cli', '2.64.0', { exe: 'gh' } as any), + ).toStrictEqual({ + packageName: 'cli/cli', + datasource: 'github-releases', + currentValue: '2.64.0', + extractVersion: '^v?(?.+)', + }); + }); + + it('should set extractVersion if tag_regex is provided', () => { + expect( + createUbiToolConfig('cargo-bins/cargo-binstall', '1.10.17', { + tag_regex: '^\\d+\\.\\d+\\.', + }), + ).toStrictEqual({ + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + currentValue: '1.10.17', + extractVersion: '^v?(?\\d+\\.\\d+\\.)', + }); + }); + + it('should trim the leading ^v from tag_regex', () => { + expect( + createUbiToolConfig('cargo-bins/cargo-binstall', '1.10.17', { + tag_regex: '^v\\d+\\.\\d+\\.', + }), + ).toStrictEqual({ + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + currentValue: '1.10.17', + extractVersion: '^v?(?\\d+\\.\\d+\\.)', + }); + }); + + it('should trim the leading ^v? from tag_regex', () => { + expect( + createUbiToolConfig('cargo-bins/cargo-binstall', '1.10.17', { + tag_regex: '^v?\\d+\\.\\d+\\.', + }), + ).toStrictEqual({ + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + currentValue: '1.10.17', + extractVersion: '^v?(?\\d+\\.\\d+\\.)', + }); + }); + }); +}); diff --git a/lib/modules/manager/mise/backends.ts b/lib/modules/manager/mise/backends.ts new file mode 100644 index 00000000000000..92f32ada30c24e --- /dev/null +++ b/lib/modules/manager/mise/backends.ts @@ -0,0 +1,223 @@ +import is from '@sindresorhus/is'; +import { regEx } from '../../../util/regex'; +import { CrateDatasource } from '../../datasource/crate'; +import { GitRefsDatasource } from '../../datasource/git-refs'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubReleasesDatasource } from '../../datasource/github-releases'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { GoDatasource } from '../../datasource/go'; +import { NpmDatasource } from '../../datasource/npm'; +import { NugetDatasource } from '../../datasource/nuget'; +import { PypiDatasource } from '../../datasource/pypi'; +import { normalizePythonDepName } from '../../datasource/pypi/common'; +import { RubygemsDatasource } from '../../datasource/rubygems'; +import type { PackageDependency } from '../types'; +import type { MiseToolOptionsSchema } from './schema'; + +export type BackendToolingConfig = Omit & + Required< + | Pick + | Pick + >; + +/** + * Create a tooling config for aqua backend + * @link https://mise.jdx.dev/dev-tools/backends/aqua.html + */ +export function createAquaToolConfig( + name: string, + version: string, +): BackendToolingConfig { + // mise supports http aqua package type but we cannot determine it from the tool name + // An error will be thrown afterwards if the package type is http + // ref: https://github.com/jdx/mise/blob/d1b9749d8f3e13ef705c1ea471d96c5935b79136/src/aqua/aqua_registry.rs#L39-L45 + return { + packageName: name, + datasource: GithubTagsDatasource.id, + // Trim the leading 'v' from both the current and extracted version + currentValue: version.replace(/^v/, ''), + extractVersion: '^v?(?.+)', + }; +} + +const cargoGitVersionRegex = regEx(/^(?tag|branch|rev):(?.+)$/); + +/** + * Create a tooling config for cargo backend + * @link https://mise.jdx.dev/dev-tools/backends/cargo.html + */ +export function createCargoToolConfig( + name: string, + version: string, +): BackendToolingConfig { + // Avoid narrowing the type of name to never + if (!(is.urlString as (value: unknown) => boolean)(name)) { + return { + packageName: name, + datasource: CrateDatasource.id, + }; + } + // tag: branch: or rev: is required for git repository url + // e.g. branch:main, tag:0.1.0, rev:abcdef + const matchGroups = cargoGitVersionRegex.exec(version)?.groups; + if (is.undefined(matchGroups)) { + return { + packageName: name, + skipReason: 'invalid-version', + }; + } + const { type, version: gitVersion } = matchGroups; + switch (type as 'tag' | 'branch' | 'rev') { + case 'tag': + return { + packageName: name, + datasource: GitTagsDatasource.id, + currentValue: gitVersion, + }; + case 'branch': + return { + packageName: name, + skipReason: 'unsupported-version', + }; + case 'rev': + return { + packageName: name, + datasource: GitRefsDatasource.id, + currentValue: gitVersion, + }; + } +} + +/** + * Create a tooling config for dotnet backend + * @link https://mise.jdx.dev/dev-tools/backends/dotnet.html + */ +export function createDotnetToolConfig(name: string): BackendToolingConfig { + return { + packageName: name, + datasource: NugetDatasource.id, + }; +} + +/** + * Create a tooling config for gem backend + * @link https://mise.jdx.dev/dev-tools/backends/gem.html + */ +export function createGemToolConfig(name: string): BackendToolingConfig { + return { + packageName: name, + datasource: RubygemsDatasource.id, + }; +} + +/** + * Create a tooling config for go backend + * @link https://mise.jdx.dev/dev-tools/backends/go.html + */ +export function createGoToolConfig(name: string): BackendToolingConfig { + return { + packageName: name, + datasource: GoDatasource.id, + }; +} + +/** + * Create a tooling config for npm backend + * @link https://mise.jdx.dev/dev-tools/backends/npm.html + */ +export function createNpmToolConfig(name: string): BackendToolingConfig { + return { + packageName: name, + datasource: NpmDatasource.id, + }; +} + +const pipxGitHubRegex = regEx(/^git\+https:\/\/github\.com\/(?.+)\.git$/); + +/** + * Create a tooling config for pipx backend + * @link https://mise.jdx.dev/dev-tools/backends/pipx.html + */ +export function createPipxToolConfig(name: string): BackendToolingConfig { + const isGitSyntax = name.startsWith('git+'); + // Does not support zip file url + // Avoid type narrowing to prevent type error + if (!isGitSyntax && (is.urlString as (value: unknown) => boolean)(name)) { + return { + packageName: name, + skipReason: 'unsupported-url', + }; + } + if (isGitSyntax || name.includes('/')) { + let repoName: string | undefined; + if (isGitSyntax) { + repoName = pipxGitHubRegex.exec(name)?.groups?.repo; + // If the url is not a github repo, treat the version as a git ref + if (is.undefined(repoName)) { + return { + packageName: name.replace(/^git\+/g, '').replaceAll(/\.git$/g, ''), + datasource: GitRefsDatasource.id, + }; + } + } else { + repoName = name; + } + return { + packageName: repoName, + datasource: GithubTagsDatasource.id, + }; + } + return { + packageName: normalizePythonDepName(name), + datasource: PypiDatasource.id, + }; +} + +const spmGitHubRegex = regEx(/^https:\/\/github.com\/(?.+).git$/); + +/** + * Create a tooling config for spm backend + * @link https://mise.jdx.dev/dev-tools/backends/spm.html + */ +export function createSpmToolConfig(name: string): BackendToolingConfig { + let repoName: string | undefined; + // Avoid type narrowing to prevent type error + if ((is.urlString as (value: unknown) => boolean)(name)) { + repoName = spmGitHubRegex.exec(name)?.groups?.repo; + // spm backend only supports github repos + if (!repoName) { + return { + packageName: name, + skipReason: 'unsupported-url', + }; + } + } + return { + packageName: repoName ?? name, + datasource: GithubReleasesDatasource.id, + }; +} + +/** + * Create a tooling config for ubi backend + * @link https://mise.jdx.dev/dev-tools/backends/ubi.html + */ +export function createUbiToolConfig( + name: string, + version: string, + toolOptions: MiseToolOptionsSchema, +): BackendToolingConfig { + return { + packageName: name, + datasource: GithubReleasesDatasource.id, + // Trim the leading 'v' from both the current and extracted version + currentValue: version.replace(/^v/, ''), + // Filter versions by tag_regex if it is specified + // ref: https://mise.jdx.dev/dev-tools/backends/ubi.html#ubi-uses-weird-versions + extractVersion: `^v?(?${ + is.string(toolOptions.tag_regex) + ? toolOptions.tag_regex.replace(/^\^?v?\??/, '') + : '.+' + })`, + }; +} diff --git a/lib/modules/manager/mise/extract.spec.ts b/lib/modules/manager/mise/extract.spec.ts index 67e2edea5303e1..ecb79bacf04b33 100644 --- a/lib/modules/manager/mise/extract.spec.ts +++ b/lib/modules/manager/mise/extract.spec.ts @@ -4,7 +4,7 @@ import { extractPackageFile } from '.'; jest.mock('../../../util/fs'); -const miseFilename = '.mise.toml'; +const miseFilename = 'mise.toml'; const mise1toml = Fixtures.get('Mise.1.toml'); @@ -103,10 +103,303 @@ describe('modules/manager/mise/extract', () => { }); }); + it('extracts tools in the default registry with backends', () => { + const content = codeBlock` + [tools] + "core:node" = "16" + "asdf:rust" = "1.82.0" + "vfox:scala" = "3.5.2" + "aqua:act" = "0.2.70" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'core:node', + currentValue: '16', + packageName: 'nodejs', + datasource: 'node-version', + }, + { + depName: 'asdf:rust', + currentValue: '1.82.0', + packageName: 'rust-lang/rust', + datasource: 'github-tags', + }, + { + depName: 'vfox:scala', + currentValue: '3.5.2', + packageName: 'lampepfl/dotty', + datasource: 'github-tags', + }, + { + depName: 'aqua:act', + currentValue: '0.2.70', + packageName: 'nektos/act', + datasource: 'github-releases', + }, + ], + }); + }); + + it('extracts aqua backend tool', () => { + const content = codeBlock` + [tools] + "aqua:BurntSushi/ripgrep" = "14.1.0" + "aqua:cli/cli" = "v2.64.0" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'aqua:BurntSushi/ripgrep', + currentValue: '14.1.0', + packageName: 'BurntSushi/ripgrep', + datasource: 'github-tags', + extractVersion: '^v?(?.+)', + }, + { + depName: 'aqua:cli/cli', + currentValue: '2.64.0', + packageName: 'cli/cli', + datasource: 'github-tags', + extractVersion: '^v?(?.+)', + }, + ], + }); + }); + + it('extracts cargo backend tools', () => { + const content = codeBlock` + [tools] + "cargo:eza" = "0.18.21" + "cargo:https://github.com/username/demo1" = "tag:v0.1.0" + "cargo:https://github.com/username/demo2" = "branch:main" + "cargo:https://github.com/username/demo3" = "rev:abcdef" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'cargo:eza', + currentValue: '0.18.21', + packageName: 'eza', + datasource: 'crate', + }, + { + depName: 'cargo:https://github.com/username/demo1', + currentValue: 'v0.1.0', + packageName: 'https://github.com/username/demo1', + datasource: 'git-tags', + }, + { + depName: 'cargo:https://github.com/username/demo2', + packageName: 'https://github.com/username/demo2', + skipReason: 'unsupported-version', + }, + { + depName: 'cargo:https://github.com/username/demo3', + currentValue: 'abcdef', + packageName: 'https://github.com/username/demo3', + datasource: 'git-refs', + }, + ], + }); + }); + + it('extracts dotnet backend tool', () => { + const content = codeBlock` + [tools] + "dotnet:GitVersion.Tool" = "5.12.0" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'dotnet:GitVersion.Tool', + currentValue: '5.12.0', + packageName: 'GitVersion.Tool', + datasource: 'nuget', + }, + ], + }); + }); + + it('extracts gem backend tool', () => { + const content = codeBlock` + [tools] + "gem:rubocop" = "1.69.2" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'gem:rubocop', + currentValue: '1.69.2', + packageName: 'rubocop', + datasource: 'rubygems', + }, + ], + }); + }); + + it('extracts go backend tool', () => { + const content = codeBlock` + [tools] + "go:github.com/DarthSim/hivemind" = "1.0.6" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'go:github.com/DarthSim/hivemind', + currentValue: '1.0.6', + packageName: 'github.com/DarthSim/hivemind', + datasource: 'go', + }, + ], + }); + }); + + it('extracts npm backend tool', () => { + const content = codeBlock` + [tools] + "npm:prettier" = "3.3.2" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'npm:prettier', + currentValue: '3.3.2', + packageName: 'prettier', + datasource: 'npm', + }, + ], + }); + }); + + it('extracts pipx backend tools', () => { + const content = codeBlock` + [tools] + "pipx:yamllint" = "1.35.0" + "pipx:psf/black" = "24.4.1" + "pipx:git+https://github.com/psf/black.git" = "24.4.1" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'pipx:yamllint', + currentValue: '1.35.0', + packageName: 'yamllint', + datasource: 'pypi', + }, + { + depName: 'pipx:psf/black', + currentValue: '24.4.1', + packageName: 'psf/black', + datasource: 'github-tags', + }, + { + depName: 'pipx:git+https://github.com/psf/black.git', + currentValue: '24.4.1', + packageName: 'psf/black', + datasource: 'github-tags', + }, + ], + }); + }); + + it('extracts spm backend tools', () => { + const content = codeBlock` + [tools] + "spm:tuist/tuist" = "4.15.0" + "spm:https://github.com/tuist/tuist.git" = "4.13.0" + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'spm:tuist/tuist', + currentValue: '4.15.0', + packageName: 'tuist/tuist', + datasource: 'github-releases', + }, + { + depName: 'spm:https://github.com/tuist/tuist.git', + currentValue: '4.13.0', + packageName: 'tuist/tuist', + datasource: 'github-releases', + }, + ], + }); + }); + + it('extracts ubi backend tools', () => { + const content = codeBlock` + [tools] + "ubi:nekto/act" = "v0.2.70" + "ubi:cli/cli" = { exe = "gh", version = "1.14.0" } + "ubi:cli/cli[exe=gh]" = "1.14.0" + "ubi:cargo-bins/cargo-binstall" = { tag_regex = "^\\\\d+\\\\.\\\\d+\\\\.", version = "1.0.0" } + "ubi:cargo-bins/cargo-binstall[tag_regex=^\\\\d+\\\\.]" = "1.0.0" + 'ubi:cargo-bins/cargo-binstall[tag_regex=^\\d+\\.\\d+\\.]' = { tag_regex = '^\\d+\\.', version = "1.0.0" } + `; + const result = extractPackageFile(content, miseFilename); + expect(result).toMatchObject({ + deps: [ + { + depName: 'ubi:nekto/act', + currentValue: '0.2.70', + packageName: 'nekto/act', + datasource: 'github-releases', + extractVersion: '^v?(?.+)', + }, + { + depName: 'ubi:cli/cli', + currentValue: '1.14.0', + packageName: 'cli/cli', + datasource: 'github-releases', + extractVersion: '^v?(?.+)', + }, + { + depName: 'ubi:cli/cli', + currentValue: '1.14.0', + packageName: 'cli/cli', + datasource: 'github-releases', + extractVersion: '^v?(?.+)', + }, + { + depName: 'ubi:cargo-bins/cargo-binstall', + currentValue: '1.0.0', + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + extractVersion: '^v?(?\\d+\\.\\d+\\.)', + }, + { + depName: 'ubi:cargo-bins/cargo-binstall', + currentValue: '1.0.0', + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + extractVersion: '^v?(?\\d+\\.)', + }, + { + depName: 'ubi:cargo-bins/cargo-binstall', + currentValue: '1.0.0', + packageName: 'cargo-bins/cargo-binstall', + datasource: 'github-releases', + extractVersion: '^v?(?\\d+\\.)', + }, + ], + }); + }); + it('provides skipReason for lines with unsupported tooling', () => { const content = codeBlock` [tools] fake-tool = '1.0.0' + 'fake:tool' = '1.0.0' `; const result = extractPackageFile(content, miseFilename); expect(result).toMatchObject({ @@ -115,6 +408,10 @@ describe('modules/manager/mise/extract', () => { depName: 'fake-tool', skipReason: 'unsupported-datasource', }, + { + depName: 'fake:tool', + skipReason: 'unsupported-datasource', + }, ], }); }); @@ -172,7 +469,7 @@ describe('modules/manager/mise/extract', () => { }); }); - it('complete .mise.toml example', () => { + it('complete mise.toml example', () => { const result = extractPackageFile(mise1toml, miseFilename); expect(result).toMatchObject({ deps: [ diff --git a/lib/modules/manager/mise/extract.ts b/lib/modules/manager/mise/extract.ts index 9aa41b16c1ae06..2d191268e0602a 100644 --- a/lib/modules/manager/mise/extract.ts +++ b/lib/modules/manager/mise/extract.ts @@ -1,12 +1,29 @@ import is from '@sindresorhus/is'; import { logger } from '../../../logger'; +import { regEx } from '../../../util/regex'; import type { ToolingConfig } from '../asdf/upgradeable-tooling'; import type { PackageDependency, PackageFileContent } from '../types'; -import type { MiseToolSchema } from './schema'; +import type { BackendToolingConfig } from './backends'; +import { + createAquaToolConfig, + createCargoToolConfig, + createDotnetToolConfig, + createGemToolConfig, + createGoToolConfig, + createNpmToolConfig, + createPipxToolConfig, + createSpmToolConfig, + createUbiToolConfig, +} from './backends'; +import type { MiseToolOptionsSchema, MiseToolSchema } from './schema'; import type { ToolingDefinition } from './upgradeable-tooling'; import { asdfTooling, miseTooling } from './upgradeable-tooling'; import { parseTomlFile } from './utils'; +// Tool names can have options in the tool name +// e.g. ubi:tamasfe/taplo[matching=full,exe=taplo] +const optionInToolNameRegex = regEx(/^(?.+?)(?:\[(?.+)\])?$/); + export function extractPackageFile( content: string, packageFile: string, @@ -24,8 +41,21 @@ export function extractPackageFile( if (tools) { for (const [name, toolData] of Object.entries(tools)) { const version = parseVersion(toolData); - const depName = name.trim(); - const toolConfig = getToolConfig(depName, version); + // Parse the tool options in the tool name + const { name: depName, options: optionsInName } = + optionInToolNameRegex.exec(name.trim())?.groups ?? { + name: name.trim(), + }; + const delimiterIndex = name.indexOf(':'); + const backend = depName.substring(0, delimiterIndex); + const toolName = depName.substring(delimiterIndex + 1); + const options = parseOptions( + optionsInName, + is.nonEmptyObject(toolData) ? toolData : {}, + ); + const toolConfig = is.null_(version) + ? null + : getToolConfig(backend, toolName, version, options); const dep = createDependency(depName, version, toolConfig); deps.push(dep); } @@ -53,18 +83,79 @@ function parseVersion(toolData: MiseToolSchema): string | null { return null; // Return null if no version is found } +function parseOptions( + optionsInName: string, + toolOptions: MiseToolOptionsSchema, +): MiseToolOptionsSchema { + const options = is.nonEmptyString(optionsInName) + ? Object.fromEntries( + optionsInName.split(',').map((option) => option.split('=', 2)), + ) + : {}; + // Options in toolOptions will override options in the tool name + return { + ...options, + ...toolOptions, + }; +} + function getToolConfig( - name: string, - version: string | null, -): ToolingConfig | null { - if (version === null) { - return null; // Early return if version is null + backend: string, + toolName: string, + version: string, + toolOptions: MiseToolOptionsSchema, +): ToolingConfig | BackendToolingConfig | null { + switch (backend) { + case '': + // If the tool name does not specify a backend, it should be a short name or an alias defined by users + return getRegistryToolConfig(toolName, version); + // We can specify core, asdf, vfox, aqua backends for tools in the default registry + // e.g. 'core:rust', 'asdf:rust', 'vfox:clang', 'aqua:act' + case 'core': + return getConfigFromTooling(miseTooling, toolName, version); + case 'asdf': + return getConfigFromTooling(asdfTooling, toolName, version); + case 'vfox': + return getRegistryToolConfig(toolName, version); + case 'aqua': + return ( + getRegistryToolConfig(toolName, version) ?? + createAquaToolConfig(toolName, version) + ); + case 'cargo': + return createCargoToolConfig(toolName, version); + case 'dotnet': + return createDotnetToolConfig(toolName); + case 'gem': + return createGemToolConfig(toolName); + case 'go': + return createGoToolConfig(toolName); + case 'npm': + return createNpmToolConfig(toolName); + case 'pipx': + return createPipxToolConfig(toolName); + case 'spm': + return createSpmToolConfig(toolName); + case 'ubi': + return createUbiToolConfig(toolName, version, toolOptions); + default: + // Unsupported backend + return null; } +} +/** + * Get the tooling config for a short name defined in the default registry + * @link https://mise.jdx.dev/registry.html + */ +function getRegistryToolConfig( + short: string, + version: string, +): ToolingConfig | null { // Try to get the config from miseTooling first, then asdfTooling return ( - getConfigFromTooling(miseTooling, name, version) ?? - getConfigFromTooling(asdfTooling, name, version) + getConfigFromTooling(miseTooling, short, version) ?? + getConfigFromTooling(asdfTooling, short, version) ); } @@ -79,26 +170,34 @@ function getConfigFromTooling( } // Return null if no toolDefinition is found return ( - (typeof toolDefinition.config === 'function' + (is.function_(toolDefinition.config) ? toolDefinition.config(version) : toolDefinition.config) ?? null - ); // Ensure null is returned instead of undefined + ); } function createDependency( name: string, version: string | null, - config: ToolingConfig | null, + config: ToolingConfig | BackendToolingConfig | null, ): PackageDependency { - if (version === null) { - return { depName: name, skipReason: 'unspecified-version' }; + if (is.null_(version)) { + return { + depName: name, + skipReason: 'unspecified-version', + }; } - if (config === null) { - return { depName: name, skipReason: 'unsupported-datasource' }; + if (is.null_(config)) { + return { + depName: name, + skipReason: 'unsupported-datasource', + }; } + return { depName: name, currentValue: version, + // Spread the config last to override other properties ...config, }; } diff --git a/lib/modules/manager/mise/index.ts b/lib/modules/manager/mise/index.ts index 6396a47dec96ce..82eba27e4695b3 100644 --- a/lib/modules/manager/mise/index.ts +++ b/lib/modules/manager/mise/index.ts @@ -1,3 +1,17 @@ +import { deduplicateArray } from '../../../util/array'; +import { CrateDatasource } from '../../datasource/crate'; +import { GitRefsDatasource } from '../../datasource/git-refs'; +import { GitTagsDatasource } from '../../datasource/git-tags'; +import { GithubReleasesDatasource } from '../../datasource/github-releases'; +import { GithubTagsDatasource } from '../../datasource/github-tags'; +import { GoDatasource } from '../../datasource/go'; +import { JavaVersionDatasource } from '../../datasource/java-version'; +import { NodeVersionDatasource } from '../../datasource/node-version'; +import { NpmDatasource } from '../../datasource/npm'; +import { NugetDatasource } from '../../datasource/nuget'; +import { PypiDatasource } from '../../datasource/pypi'; +import { RubyVersionDatasource } from '../../datasource/ruby-version'; +import { RubygemsDatasource } from '../../datasource/rubygems'; import { supportedDatasources as asdfSupportedDatasources } from '../asdf'; export { extractPackageFile } from './extract'; @@ -9,5 +23,29 @@ export const defaultConfig = { fileMatch: ['(^|/)\\.?mise\\.toml$', '(^|/)\\.?mise/config\\.toml$'], }; -// Re-use the asdf datasources, as mise and asdf support the same plugins. -export const supportedDatasources = asdfSupportedDatasources; +const backendDatasources = { + core: [ + GithubReleasesDatasource.id, + GithubTagsDatasource.id, + JavaVersionDatasource.id, + NodeVersionDatasource.id, + RubyVersionDatasource.id, + ], + // Re-use the asdf datasources, as mise and asdf support the same plugins. + asdf: asdfSupportedDatasources, + aqua: [GithubTagsDatasource.id], + cargo: [CrateDatasource.id, GitTagsDatasource.id, GitRefsDatasource.id], + dotnet: [NugetDatasource.id], + gem: [RubygemsDatasource.id], + go: [GoDatasource.id], + npm: [NpmDatasource.id], + pipx: [PypiDatasource.id, GithubTagsDatasource.id, GitRefsDatasource.id], + spm: [GithubReleasesDatasource.id], + ubi: [GithubReleasesDatasource.id], + // not supported + vfox: [], +}; + +export const supportedDatasources = deduplicateArray( + Object.values(backendDatasources).flat(), +); diff --git a/lib/modules/manager/mise/readme.md b/lib/modules/manager/mise/readme.md index 4def797b88ad3f..c0c53354117f79 100644 --- a/lib/modules/manager/mise/readme.md +++ b/lib/modules/manager/mise/readme.md @@ -1,9 +1,4 @@ -Renovate can update the [mise](https://mise.jdx.dev/configuration.html#mise-toml) `.mise.toml` file. - -Renovate's `mise` manager can version these tools: - - - +Renovate can update the [mise](https://mise.jdx.dev/configuration.html#mise-toml) `mise.toml` file. ### Renovate only updates primary versions @@ -30,16 +25,11 @@ To maintain consistency and reliability, Renovate opts to only manage the _first This follows the same workflow that Renovate's `asdf` manager uses. -### Plugin/tool support - -Renovate uses: - -- [mise's plugins](https://github.com/jdx/mise/tree/main/src/plugins/core) -- [asdf's plugins](https://mise.jdx.dev/registry.html) +### Short names support -to understand and manage tool versioning. +Renovate uses [mise registry](https://mise.jdx.dev/registry.html) to understand tools short names. -Support for new tools/plugins needs to be _manually_ added to Renovate's logic. +Support for new tool short names needs to be _manually_ added to Renovate's logic. #### Adding new tool support @@ -48,8 +38,67 @@ There are 2 ways to integrate versioning for a new tool: - Renovate's `mise` manager: ensure upstream `mise` supports the tool, then add support to the `mise` manager in Renovate - Renovate's `asdf` manager: improve the `asdf` manager in Renovate, which automatically extends support to `mise` -If `mise` adds support for more tools via its own [core plugins](https://mise.jdx.dev/plugins.html#core-plugins), you can create a PR to extend Renovate's `mise` manager to add support for the new tooling. +If `mise` adds support for more tools via its own [core tools](https://mise.jdx.dev/core-tools.html), you can create a PR to extend Renovate's `mise` manager to add support for the new core tools. + +If you are wanting to add support for an other tools' short names to `mise`, you can create a PR to extend Renovate's `asdf` manager, which indirectly helps Renovate's `mise` manager as well. + +Note that some tools in the registry are not using the `asdf` backend. We are currently not supporting those tool short names. + +TODO: Change the registry lookup. + +### Backends support + +Renovate's `mise` manager supports the following [backends](https://mise.jdx.dev/dev-tools/backends/): + +- [`core`](https://mise.jdx.dev/core-tools.html) +- [`asdf`](https://mise.jdx.dev/dev-tools/backends/asdf.html) +- [`aqua`](https://mise.jdx.dev/dev-tools/backends/aqua.html) +- [`cargo`](https://mise.jdx.dev/dev-tools/backends/cargo.html) +- [`go`](https://mise.jdx.dev/dev-tools/backends/go.html) +- [`npm`](https://mise.jdx.dev/dev-tools/backends/npm.html) +- [`pipx`](https://mise.jdx.dev/dev-tools/backends/pipx.html) +- [`spm`](https://mise.jdx.dev/dev-tools/backends/spm.html) +- [`ubi`](https://mise.jdx.dev/dev-tools/backends/ubi.html) +- [`vfox`](https://mise.jdx.dev/dev-tools/backends/vfox.html) -You may be able to add support for new tooling upstream in the core plugins - create an issue and see if the community agrees whether it belongs there, or if it would be better as an `asdf-` plugin. +#### Limitations -If you are wanting to add support for an existing `asdf-x` plugin to `mise`, you can create a PR to extend Renovate's `asdf` manager, which indirectly helps Renovate's `mise` manager as well. +Renovate's `mise` manager does not support the following tool syntax: + +- `asdf` and `vfox` plugins + e.g. `asdf:asdf:mise-plugins/asdf-yarn` or `vfox:vfox:version-fox/vfox-elixir` + Short names with backends like `asdf:yarn` or `vfox:elixir` are supported if the short names are supported. + +- `aqua` packages with `http` [package type](https://aquaproj.github.io/docs/reference/registry-config/#package-types). + However if the short name using `aqua` backend is supported by Renovate, it will be updated. + e.g. [`aqua:helm/helm`](https://github.com/aquaproj/aqua-registry/blob/main/pkgs/helm/helm/registry.yaml) is not supported, but `helm` or `aqua:helm` is supported. + +- `aqua` packages with [`version_filter`](https://aquaproj.github.io/docs/reference/registry-config/version-prefix). + We don't read the aqua registry itself, so we can't support this feature. + If some packages using `version_filter` like [`aqua:biomejs/biome`](https://github.com/aquaproj/aqua-registry/blob/main/pkgs/biomejs/biome/registry.yaml) are not updated or updated incorrectly, set `extractVersion` in the Renovate config manually like below. + + ```json + { + "packageRules": [ + { + "depNames": ["aqua:biomejs/biome"], + "extractVersion": "cli/(?.+)" + } + ] + } + ``` + +- `ubi` backend tools with [`tag_regex`](https://mise.jdx.dev/dev-tools/backends/ubi.html#ubi-uses-weird-versions) option. + The `tag_regex` option is used as `extractVersion`, but the regex engines are not the same between mise and Renovate. + If the version is not updated or updated incorrectly, override `extractVersion` manually in the renovate config. + +- Versions with `v` prefix. + mise automatically strips the `v` prefix from versions, but Renovate does not. + If the version is not updated or updated incorrectly, set `extractVersion` to `v(?.+)` in the Renovate config. + +### Supported default registry tool short names + +Renovate's `mise` manager can only version these tool short names: + + + diff --git a/lib/modules/manager/mise/schema.ts b/lib/modules/manager/mise/schema.ts index b4614ebff9ca83..2e264fd92791be 100644 --- a/lib/modules/manager/mise/schema.ts +++ b/lib/modules/manager/mise/schema.ts @@ -1,9 +1,17 @@ import { z } from 'zod'; import { Toml } from '../../../util/schema-utils'; +const MiseToolOptionsSchema = z.object({ + // ubi backend only + tag_regex: z.string().optional(), +}); +export type MiseToolOptionsSchema = z.infer; + const MiseToolSchema = z.union([ z.string(), - z.object({ version: z.string().optional() }), + MiseToolOptionsSchema.extend({ + version: z.string().optional(), + }), z.array(z.string()), ]); export type MiseToolSchema = z.infer;