mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 15:58:48 +00:00
9fc451c44d
Migrate pub, gleam, helmfile, pixi, mise, poetry, pep621/pdm and
pep621/uv onto `updateLockFile()`, so each manager keeps only its
command and `ExecOptions` construction.
Behaviour changes:
- Artifact error text now comes from `artifactErrorMessageFromExecError`,
i.e. a non-blank `err.stderr`, else a non-blank `err.stdout`, else
`err.message`. Previously pixi used `${err}` ("Error: " prefix), mise
joined stdout, stderr and message, and poetry rendered
"<stdout>\n<stderr>" (literally "undefined\nundefined" when the error
carried neither).
- helmfile, pixi, poetry, pdm and uv now return null instead of an
addition when the lock file is missing or empty after the run, which
is what gleam and mise already did.
- Unchanged/missing lock file debug messages are now uniformly
"<lockFileName> is unchanged" / "No <lockFileName> found". pub gains
such a message, mise loses "Returning updated mise lock file" and
poetry loses "Returning updated <lockFileName>".
256 lines
8.9 KiB
TypeScript
256 lines
8.9 KiB
TypeScript
import { isNonEmptyStringAndNotWhitespace } from '@sindresorhus/is';
|
|
import { quote } from 'shlex';
|
|
import upath from 'upath';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { findGithubToken } from '../../../util/check-token.ts';
|
|
import { exec } from '../../../util/exec/index.ts';
|
|
import type {
|
|
ExecOptions,
|
|
ExtraEnv,
|
|
ToolConstraint,
|
|
} from '../../../util/exec/types.ts';
|
|
import { readLocalFile, writeLocalFile } from '../../../util/fs/index.ts';
|
|
import * as hostRules from '../../../util/host-rules.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { api as miseVersioning } from '../../versioning/semver/index.ts';
|
|
import type {
|
|
ToolConstraintsConfig,
|
|
UpdateArtifact,
|
|
UpdateArtifactsResult,
|
|
} from '../types.ts';
|
|
import {
|
|
artifactErrorResult,
|
|
resolveToolConstraint,
|
|
updateLockFile,
|
|
} from '../util.ts';
|
|
import { getConfigType, getLockFileName } from './lockfile.ts';
|
|
|
|
/**
|
|
* First mise release that supports the features this manager relies on:
|
|
* `MISE_SAFE=1` safe mode (a hard boundary against project config executing
|
|
* code during `mise lock`) and `mise lock --bump` (advancing fuzzy selectors).
|
|
*
|
|
* Safe mode (jdx/mise#11146) and lockfile bumping (jdx/mise#11145) merged after
|
|
* v2026.7.11 and first shipped in v2026.7.12.
|
|
*
|
|
* @see https://github.com/jdx/mise/pull/11146
|
|
* @see https://github.com/jdx/mise/pull/11145
|
|
* @see https://mise.jdx.dev/configuration/settings.html#safe
|
|
*/
|
|
const MISE_SAFE_MODE_MIN_VERSION = '2026.7.12';
|
|
|
|
/**
|
|
* Detects the mise version that will actually run, by executing `mise version`.
|
|
* `mise version` only prints the binary's version — it does not load or execute
|
|
* project configuration — so it is safe to run even without allowlisting.
|
|
*
|
|
* Output looks like `2026.7.12 macos-arm64 (2026-07-21)`; we take the leading
|
|
* `major.minor.patch`. Returns `null` if the probe fails or cannot be parsed,
|
|
* in which case callers fall back to the conservative (pre-safe-mode) behavior.
|
|
*/
|
|
async function detectMiseVersion(
|
|
execOptions: ExecOptions,
|
|
): Promise<string | null> {
|
|
try {
|
|
const { stdout } = await exec('mise version', execOptions);
|
|
const version = regEx(/\d+\.\d+\.\d+/).exec(stdout)?.[0];
|
|
if (version && miseVersioning.isVersion(version)) {
|
|
return version;
|
|
}
|
|
logger.debug({ stdout }, 'Could not parse mise version output');
|
|
return null;
|
|
} catch (err) {
|
|
logger.debug({ err }, 'Failed to determine mise version');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** True when `version` is at or above the safe-mode / `--bump` release. */
|
|
function versionSupportsSafeFeatures(version: string | null): boolean {
|
|
return (
|
|
!!version &&
|
|
(miseVersioning.equals(version, MISE_SAFE_MODE_MIN_VERSION) ||
|
|
miseVersioning.isGreaterThan(version, MISE_SAFE_MODE_MIN_VERSION))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Resolver tools that mise may invoke while running `mise lock`, for instance to convert an inexact version like `1` against the npm registry.
|
|
*
|
|
* NOTE: this will install all relevant tools, regardless of what the given mise configuration uses.
|
|
*
|
|
* In safe mode these are unnecessary: npm version listing uses mise's built-in
|
|
* registry HTTP client and go runs with `GOTOOLCHAIN=local`, so `mise lock`
|
|
* resolves versions over HTTP without shelling out to node/npm/ruby.
|
|
*
|
|
* @see https://mise.jdx.dev/dev-tools/backends/npm.html
|
|
* @see https://mise.jdx.dev/dev-tools/backends/go.html
|
|
* @see https://mise.jdx.dev/dev-tools/backends/gem.html
|
|
*/
|
|
async function getMiseLockToolConstraints(
|
|
config: ToolConstraintsConfig,
|
|
miseConstraint: string | undefined,
|
|
safeMode = false,
|
|
): Promise<ToolConstraint[]> {
|
|
const miseToolConstraint: ToolConstraint = {
|
|
toolName: 'mise',
|
|
constraint: miseConstraint,
|
|
};
|
|
if (safeMode) {
|
|
return [miseToolConstraint];
|
|
}
|
|
return [
|
|
miseToolConstraint,
|
|
{
|
|
toolName: 'node',
|
|
constraint: await resolveToolConstraint(config, 'node'),
|
|
},
|
|
{
|
|
toolName: 'npm',
|
|
constraint: await resolveToolConstraint(config, 'npm'),
|
|
},
|
|
{
|
|
// the golang tool is constrained by the `go` constraint
|
|
toolName: 'golang',
|
|
constraint: await resolveToolConstraint(config, 'go'),
|
|
},
|
|
{
|
|
toolName: 'ruby',
|
|
constraint: await resolveToolConstraint(config, 'ruby'),
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Updates mise lock files when dependencies are updated.
|
|
* Runs `mise lock` for lock file maintenance or `mise lock <tools>` for targeted updates.
|
|
*/
|
|
export async function updateArtifacts({
|
|
packageFileName,
|
|
updatedDeps,
|
|
newPackageFileContent,
|
|
newLockFileContent,
|
|
config,
|
|
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
|
|
const lockFileName = getLockFileName(packageFileName);
|
|
const originalLockFileContent = await readLocalFile(lockFileName, 'utf8');
|
|
const currentLockFileContent = newLockFileContent ?? originalLockFileContent;
|
|
if (!currentLockFileContent) {
|
|
logger.debug({ lockFileName }, 'No mise lock file found');
|
|
return null;
|
|
}
|
|
|
|
// `mise lock` normally executes the project's mise config, so it requires
|
|
// `allowedUnsafeExecutions`. When the mise that will run supports safe mode,
|
|
// we instead run with `MISE_SAFE=1`, which is a hard boundary against the
|
|
// config executing code — so the allowlist is not required on that path.
|
|
const allowlist = GlobalConfig.get('allowedUnsafeExecutions');
|
|
const miseAllowlisted = allowlist.includes('mise');
|
|
|
|
// The mise version is needed to decide safe mode (untrusted path) and
|
|
// `mise lock --bump` (lock file maintenance). Detect it at runtime rather
|
|
// than requiring a pinned `constraints.mise`. Skip the probe when it cannot
|
|
// change the outcome: an allowlisted, non-maintenance run behaves the same
|
|
// regardless of version.
|
|
const miseConstraint = await resolveToolConstraint(config, 'mise');
|
|
let miseSupportsSafeFeatures = false;
|
|
if (!miseAllowlisted || config.isLockFileMaintenance) {
|
|
const miseVersion = await detectMiseVersion({
|
|
cwdFile: packageFileName,
|
|
toolConstraints: [{ toolName: 'mise', constraint: miseConstraint }],
|
|
docker: {},
|
|
});
|
|
miseSupportsSafeFeatures = versionSupportsSafeFeatures(miseVersion);
|
|
}
|
|
|
|
const safeMode = !miseAllowlisted && miseSupportsSafeFeatures;
|
|
if (!miseAllowlisted && !safeMode) {
|
|
logger.once.warn(
|
|
{ safeModeMinVersion: MISE_SAFE_MODE_MIN_VERSION },
|
|
'`mise lock` requires either `mise` in `allowedUnsafeExecutions`, or a mise version that supports safe mode',
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const { isLocal, env } = getConfigType(packageFileName);
|
|
const localFlag = isLocal ? ' --local' : '';
|
|
|
|
let lockCmd: string;
|
|
if (config.isLockFileMaintenance) {
|
|
// Lock file maintenance means "update to the latest versions". Plain
|
|
// `mise lock` only refreshes metadata for already-locked versions, so use
|
|
// `mise lock --bump` to also advance fuzzy selectors (e.g. `node = "22"`)
|
|
// to the latest matching version. `--bump` requires a new enough mise, so
|
|
// fall back to a plain refresh when the version cannot be guaranteed.
|
|
const bumpFlag = miseSupportsSafeFeatures ? ' --bump' : '';
|
|
lockCmd = `mise lock${localFlag}${bumpFlag}`;
|
|
} else {
|
|
const tools = updatedDeps
|
|
.map(({ depName }) => depName)
|
|
.filter(isNonEmptyStringAndNotWhitespace)
|
|
.map(quote)
|
|
.join(' ');
|
|
lockCmd = tools
|
|
? `mise lock${localFlag} ${tools}`
|
|
: `mise lock${localFlag}`;
|
|
}
|
|
|
|
const extraEnv: ExtraEnv = {};
|
|
if (env) {
|
|
extraEnv.MISE_ENV = env;
|
|
}
|
|
if (safeMode) {
|
|
extraEnv.MISE_SAFE = '1';
|
|
}
|
|
const token = findGithubToken(
|
|
hostRules.find({
|
|
hostType: 'github',
|
|
url: 'https://api.github.com/',
|
|
}),
|
|
);
|
|
if (token) {
|
|
extraEnv.GITHUB_TOKEN = token;
|
|
}
|
|
|
|
const execOptions: ExecOptions = {
|
|
cwdFile: packageFileName,
|
|
extraEnv,
|
|
toolConstraints: await getMiseLockToolConstraints(
|
|
config,
|
|
miseConstraint,
|
|
safeMode,
|
|
),
|
|
docker: {},
|
|
};
|
|
|
|
// `mise trust` is only needed on the allowlisted path. In safe mode mise
|
|
// loads untrusted config without a trust prompt (the config is inert — it
|
|
// can neither execute code nor inject environment), so the trust step is
|
|
// unnecessary and is skipped.
|
|
const commands = safeMode
|
|
? [lockCmd]
|
|
: [`mise trust ${quote(upath.basename(packageFileName))}`, lockCmd];
|
|
|
|
try {
|
|
if (newLockFileContent) {
|
|
await writeLocalFile(lockFileName, newLockFileContent);
|
|
}
|
|
return await updateLockFile({
|
|
lockFileName,
|
|
existingLockFileContent: originalLockFileContent,
|
|
packageFile: { path: packageFileName, contents: newPackageFileContent },
|
|
run: () => exec(commands, execOptions),
|
|
});
|
|
} catch (err) {
|
|
/* v8 ignore if -- defensive rethrow, not worth testing */
|
|
if (err.message === TEMPORARY_ERROR) {
|
|
throw err;
|
|
}
|
|
|
|
logger.warn({ err, lockFileName }, 'Error updating mise lock file');
|
|
return artifactErrorResult(lockFileName, err);
|
|
}
|
|
}
|