mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 17:51:49 +00:00
94f0219613
* feat(manager/mise): run mise lock in safe mode without allowedUnsafeExecutions When `constraints.mise` pins a version that supports mise safe mode (`MISE_SAFE=1`), run `mise lock` with safe mode enabled instead of requiring `mise` in `allowedUnsafeExecutions`. Safe mode is a hard boundary against project config executing code (templates exec/read_file, _.source, hooks, tasks, asdf scripts, plugin installs are all refused), so it — not the allowlist — protects the host. The existing allowlisted path is unchanged: allowlisted repos run `mise lock` exactly as before, without forcing safe mode, so configs that legitimately execute code during locking keep working. Safe mode also removes the need to pre-install node/npm/golang/ruby as resolver tools, since npm version listing uses mise's built-in registry HTTP client and go runs with GOTOOLCHAIN=local. Refs: jdx/mise#11146 (safe mode), jdx/mise#11145 (lock --bump) * fix: address oxlint and pin safe-mode min version to 2026.7.12 - logger.warn requires a static message with interpolated values in the metadata object (oxlint rule); move the version into metadata - set MISE_SAFE_MODE_MIN_VERSION to 2026.7.12, the expected first release containing safe mode (merged after v2026.7.11) * feat(manager/mise): use mise lock --bump for lock file maintenance lockFileMaintenance means "update to the latest versions", but plain `mise lock` only refreshes metadata for already-locked versions. Use `mise lock --bump` so fuzzy selectors (e.g. node = "22") advance to the latest matching version. `--bump` requires the same minimum mise version as safe mode, so it is gated on the mise constraint and falls back to a plain refresh otherwise. Generalizes the version-gate helper (miseConstraintGuarantees) shared by safe mode and --bump. * refactor(manager/mise): detect mise version at runtime instead of via constraints Determine safe-mode / --bump support by running `mise version` (which does not load or execute project config) rather than requiring a pinned `constraints.mise`. This works off whatever mise is actually installed, so no extra configuration is needed. The probe is skipped on the allowlisted, non-maintenance path, where the version cannot change the outcome — keeping that hot path unchanged. * feat(manager/mise): skip mise trust in safe mode Safe mode now makes an untrusted config fully inert and no longer requires trust to load it (jdx/mise#11151), so on the safe-mode path `mise trust` is unnecessary and is skipped — `mise lock` runs with `MISE_SAFE=1` alone. The allowlisted path is unchanged and still runs `mise trust`. * feat(manager/mise): confirm safe mode min version against v2026.7.12 release MISE_SAFE=1 safe mode and `mise lock --bump` first shipped in mise v2026.7.12 (released 2026-07-23), confirming the assumed MISE_SAFE_MODE_MIN_VERSION. Resolve the pending-release TODO and align the test version fixture with the real release date. Validated end-to-end against the real v2026.7.12 binary: `mise version` output matches the parser, `MISE_SAFE=1 mise lock` refreshes an untrusted config without trust, `mise lock --bump` advances fuzzy selectors, and project [env] is ignored under safe mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(manager/mise): cover mise version probe failure path Add a test for the detectMiseVersion catch branch (exec throwing), so the non-allowlisted path falls back to conservative behavior. Restores 100% patch coverage for the mise manager. * Update lib/modules/manager/mise/readme.md Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> * Update lib/modules/manager/mise/readme.md Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> * docs(mise): fix safe mode section link --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
246 lines
8.6 KiB
TypeScript
246 lines
8.6 KiB
TypeScript
import { isNonEmptyStringAndNotWhitespace, isString } 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 {
|
|
ConstraintName,
|
|
ExecOptions,
|
|
ExtraEnv,
|
|
ToolConstraint,
|
|
} from '../../../util/exec/types.ts';
|
|
import { readLocalFile } 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 { UpdateArtifact, UpdateArtifactsResult } from '../types.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
|
|
*/
|
|
function getMiseLockToolConstraints(
|
|
constraints?: Partial<Record<ConstraintName, string>> | null,
|
|
safeMode = false,
|
|
): ToolConstraint[] {
|
|
const miseConstraint: ToolConstraint = {
|
|
toolName: 'mise',
|
|
constraint: constraints?.mise,
|
|
};
|
|
if (safeMode) {
|
|
return [miseConstraint];
|
|
}
|
|
return [
|
|
miseConstraint,
|
|
{ toolName: 'node', constraint: constraints?.node },
|
|
{ toolName: 'npm', constraint: constraints?.npm },
|
|
{ toolName: 'golang', constraint: constraints?.go },
|
|
{ toolName: 'ruby', constraint: constraints?.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,
|
|
config,
|
|
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
|
|
const lockFileName = getLockFileName(packageFileName);
|
|
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
|
|
if (!existingLockFileContent) {
|
|
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.
|
|
let miseSupportsSafeFeatures = false;
|
|
if (!miseAllowlisted || config.isLockFileMaintenance) {
|
|
const miseVersion = await detectMiseVersion({
|
|
cwdFile: packageFileName,
|
|
toolConstraints: [
|
|
{ toolName: 'mise', constraint: config.constraints?.mise },
|
|
],
|
|
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: getMiseLockToolConstraints(config.constraints, 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 {
|
|
await exec(commands, execOptions);
|
|
const newLockFileContent = await readLocalFile(lockFileName, 'utf8');
|
|
if (!newLockFileContent || existingLockFileContent === newLockFileContent) {
|
|
return null;
|
|
}
|
|
|
|
logger.debug({ lockFileName }, 'Returning updated mise lock file');
|
|
return [
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: lockFileName,
|
|
contents: newLockFileContent,
|
|
},
|
|
},
|
|
];
|
|
} catch (err) {
|
|
// istanbul ignore if: not worth testing
|
|
if (err.message === TEMPORARY_ERROR) {
|
|
throw err;
|
|
}
|
|
|
|
const errorOutput = [err.stdout, err.stderr, err.message]
|
|
.filter(isString)
|
|
.join('\n');
|
|
|
|
logger.warn({ err, lockFileName }, 'Error updating mise lock file');
|
|
return [
|
|
{
|
|
artifactError: {
|
|
fileName: lockFileName,
|
|
stderr: errorOutput,
|
|
},
|
|
},
|
|
];
|
|
}
|
|
}
|