Files
renovate/lib/modules/manager/mise/extract.ts
Pieter Willekens 141e136869 feat(manager/mise): support gitlab backend (#44116)
* feat(manager/mise): support gitlab backend

Maps the mise `gitlab` backend (e.g. `gitlab:gitlab-org/cli`) to the gitlab-releases datasource, mirroring the github backend including version_prefix support.

* refactor(manager/mise): use optional instead of Nullish for tool options

TOML has no null type, so MiseToolOptions values are only ever a string or absent. .optional() models that exactly; Nullish only adds value for sources that can emit explicit null.

* fix(manager/mise): use RegExp.escape in gitlab backend

Main switched `createGithubToolConfig` from the `escapeRegExp` util to
`RegExp.escape` and dropped the import, which left the newly added
`createGitlabToolConfig` referencing an undefined symbol after the merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Michael Kriese <michael.kriese@mend.io>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:04:37 +00:00

309 lines
8.7 KiB
TypeScript

import {
isArray,
isFunction,
isNonEmptyObject,
isNonEmptyString,
isObject,
isString,
} from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import { readLocalFile } from '../../../util/fs/index.ts';
import { coerceObject } from '../../../util/object.ts';
import { regEx } from '../../../util/regex.ts';
import type { StaticTooling } from '../asdf/types.ts';
import type { PackageDependency, PackageFileContent } from '../types.ts';
import {
createAquaToolConfig,
createCargoToolConfig,
createDotnetToolConfig,
createGemToolConfig,
createGithubToolConfig,
createGitlabToolConfig,
createGoToolConfig,
createNpmToolConfig,
createPipxToolConfig,
createSpmToolConfig,
createUbiToolConfig,
} from './backends.ts';
import { getLockFileName, getLockedVersion } from './lockfile.ts';
import type { MiseTool, MiseToolOptions } from './schema.ts';
import { MiseLockFile } from './schema.ts';
import type { BackendToolingConfig, ToolingDefinition } from './types.ts';
import {
asdfTooling,
getOrderedMiseRegistryBackends,
miseTooling,
} from './upgradeable-tooling.ts';
import { parseTomlFile } from './utils.ts';
// Tool names can have options in the tool name
// e.g. ubi:tamasfe/taplo[matching=full,exe=taplo]
const optionInToolNameRegex = regEx(/^(?<name>.+?)(?:\[(?<options>.+)\])?$/);
/**
* Extracts mise tool dependencies from a mise configuration file.
* Supports various backends (core, asdf, aqua, cargo, etc.) and
* extracts locked versions when a corresponding lock file exists.
*/
export async function extractPackageFile(
content: string,
packageFile: string,
): Promise<PackageFileContent | null> {
logger.trace(`mise.extractPackageFile(${packageFile})`);
const misefile = parseTomlFile(content, packageFile);
if (!misefile) {
return null;
}
const deps: PackageDependency[] = [];
for (const [name, toolData] of Object.entries(misefile.tools)) {
deps.push(extractToolEntry(name, toolData, 'tools'));
}
for (const [taskName, taskData] of Object.entries(misefile.tasks)) {
for (const [name, toolData] of Object.entries(
coerceObject(taskData.tools),
)) {
deps.push(extractToolEntry(name, toolData, `task-${taskName}-tools`));
}
}
if (!deps.length) {
return null;
}
const result: PackageFileContent = { deps };
const lockFileName = getLockFileName(packageFile);
const lockFileContent = await readLocalFile(lockFileName, 'utf8');
if (lockFileContent) {
const lockFileParsed = MiseLockFile.safeParse(lockFileContent);
if (lockFileParsed.success) {
result.lockFiles = [lockFileName];
for (const dep of deps) {
const lockedVersion = getLockedVersion(
lockFileParsed.data,
dep.depName!,
);
if (lockedVersion) {
dep.lockedVersion = lockedVersion;
}
}
} else {
logger.debug(
{ lockFileName, err: lockFileParsed.error },
'Failed to parse mise lock file',
);
}
}
return result;
}
function parseVersion(toolData: MiseTool): string | null {
if (isNonEmptyString(toolData)) {
// Handle the string case
// e.g. 'erlang = "23.3"'
return toolData;
}
if (isArray(toolData, isString)) {
// Handle the array case
// e.g. 'erlang = ["23.3", "24.0"]'
return toolData.length ? toolData[0] : null; // Get the first version in the array
}
if (isObject(toolData) && isNonEmptyString(toolData.version)) {
// Handle the object case with a string version
// e.g. 'python = { version = "3.11.2" }'
return toolData.version;
}
return null; // Return null if no version is found
}
function parseOptions(
optionsInName: string,
toolOptions: MiseToolOptions,
): MiseToolOptions {
const options = isNonEmptyString(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(
backend: string,
toolName: string,
version: string,
toolOptions: MiseToolOptions,
): StaticTooling | 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
const staticResult = getRegistryToolConfig(toolName, version);
if (staticResult) {
return staticResult;
}
// Otherwise, see if we have any known short tool names that are in the `mise-registry.json` data file
const backends = getOrderedMiseRegistryBackends(toolName);
// prioritise the github backend as the best source for data
if (backends.github) {
const result = getToolConfig(
'github',
backends.github,
version,
toolOptions,
);
// v8 ignore else -- TODO: add test #40625
if (result !== null) {
return result;
}
}
for (const [backendType, backendName] of Object.entries(backends)) {
const result = getToolConfig(
backendType,
backendName,
version,
toolOptions,
);
// v8 ignore else -- TODO: add test #40625
if (result !== null) {
return result;
}
}
return null;
}
// 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 'github':
return createGithubToolConfig(toolName, version, toolOptions);
case 'gitlab':
return createGitlabToolConfig(toolName, version, toolOptions);
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,
): StaticTooling | null {
// Try to get the config from miseTooling first, then asdfTooling
return (
getConfigFromTooling(miseTooling, short, version) ??
getConfigFromTooling(asdfTooling, short, version)
);
}
function getConfigFromTooling(
toolingSource: Record<string, ToolingDefinition>,
name: string,
version: string,
): StaticTooling | null {
const toolDefinition = toolingSource[name];
if (!toolDefinition) {
return null;
} // Return null if no toolDefinition is found
return (
(isFunction(toolDefinition.config)
? toolDefinition.config(version)
: toolDefinition.config) ?? null
); // Ensure null is returned instead of undefined
}
function extractToolEntry(
name: string,
toolData: MiseTool,
depType: string,
): PackageDependency {
const version = parseVersion(toolData);
const { name: depName, options: optionsInName } = optionInToolNameRegex.exec(
name.trim(),
)!.groups!;
const delimiterIndex = depName.indexOf(':');
const backend = depName.substring(0, delimiterIndex);
const toolName = depName.substring(delimiterIndex + 1);
const options = parseOptions(
optionsInName,
isNonEmptyObject(toolData) ? toolData : {},
);
const toolConfig =
version === null
? null
: getToolConfig(backend, toolName, version, options);
return createDependency(depName, version, toolConfig, depType);
}
function createDependency(
name: string,
version: string | null,
config: StaticTooling | BackendToolingConfig | null,
depType: string,
): PackageDependency {
if (version === null) {
return {
depName: name,
depType,
skipReason: 'unspecified-version',
};
}
if (config === null) {
return {
depName: name,
depType,
skipReason: 'unsupported-datasource',
};
}
return {
depName: name,
depType,
currentValue: version,
// Spread the config last to override other properties
...config,
};
}