mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +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>
783 lines
23 KiB
TypeScript
783 lines
23 KiB
TypeScript
import upath from 'upath';
|
|
import { mockDeep } from 'vitest-mock-extended';
|
|
import { envMock, mockExecAll, mockExecSequence } from '~test/exec-util.ts';
|
|
import { env, fs, hostRules } from '~test/util.ts';
|
|
import { GlobalConfig } from '../../../config/global.ts';
|
|
import type {
|
|
InternalGlobalConfigOptions,
|
|
RepoGlobalConfig,
|
|
} from '../../../config/types.ts';
|
|
import { TEMPORARY_ERROR } from '../../../constants/error-messages.ts';
|
|
import * as docker from '../../../util/exec/docker/index.ts';
|
|
import * as _datasource from '../../datasource/index.ts';
|
|
import type { UpdateArtifactsConfig } from '../types.ts';
|
|
import { updateArtifacts } from './artifacts.ts';
|
|
import * as lockfile from './lockfile.ts';
|
|
import { updateLockedDependency } from './update-locked.ts';
|
|
|
|
const datasource = vi.mocked(_datasource);
|
|
|
|
vi.mock('../../datasource/index.ts', () => mockDeep());
|
|
vi.mock('../../../util/exec/env.ts');
|
|
vi.mock('../../../util/fs/index.ts');
|
|
|
|
const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = {
|
|
// `join` fixes Windows CI
|
|
localDir: upath.join('/tmp/github/some/repo'),
|
|
cacheDir: upath.join('/tmp/renovate/cache'),
|
|
containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'),
|
|
binarySource: 'global',
|
|
allowedUnsafeExecutions: ['mise'],
|
|
};
|
|
|
|
process.env.CONTAINERBASE = 'true';
|
|
|
|
const config: UpdateArtifactsConfig = {};
|
|
const lockMaintenanceConfig = { ...config, isLockFileMaintenance: true };
|
|
const trustCmd = 'mise trust mise.toml';
|
|
const updateToolCmd = 'mise lock node';
|
|
const trustLocalCmd = 'mise trust mise.local.toml';
|
|
const trustEnvCmd = 'mise trust mise.test.toml';
|
|
const trustEnvLocalCmd = 'mise trust mise.test.local.toml';
|
|
const trustSubdirCmd = 'mise trust mise.toml';
|
|
const updateMultipleToolsCmd = 'mise lock node python';
|
|
const lockfileMaintenanceCmd = 'mise lock';
|
|
const miseVersionCmd = 'mise version';
|
|
// `mise version` output for a release that supports safe mode / `mise lock --bump`
|
|
const safeMiseVersionOutput = {
|
|
stdout: '2026.7.12 linux-x64 (2026-07-23)',
|
|
stderr: '',
|
|
};
|
|
|
|
describe('modules/manager/mise/artifacts', () => {
|
|
beforeEach(() => {
|
|
env.getChildProcessEnv.mockReturnValue({
|
|
...envMock.basic,
|
|
LANG: 'en_US.UTF-8',
|
|
LC_ALL: 'en_US',
|
|
});
|
|
GlobalConfig.set(adminConfig);
|
|
docker.resetPrefetchedImages();
|
|
hostRules.clear();
|
|
});
|
|
|
|
it('returns null if lock file does not exist', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce(null);
|
|
const execSnapshots = mockExecAll();
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(execSnapshots).toEqual([]);
|
|
});
|
|
|
|
it('returns null when mise is not allowlisted and version cannot be determined', async () => {
|
|
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
|
|
fs.readLocalFile.mockResolvedValueOnce('existing content');
|
|
// default mock returns empty stdout, so the version probe cannot parse a version
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
|
|
});
|
|
|
|
it('runs mise lock with MISE_SAFE when a safe-mode mise version is detected and not allowlisted', async () => {
|
|
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll(safeMiseVersionOutput);
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
contents: expect.stringContaining('version = "24.16.0"'),
|
|
path: 'mise.lock',
|
|
type: 'addition',
|
|
},
|
|
},
|
|
]);
|
|
// safe mode does not require trust, so `mise trust` is not run
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: miseVersionCmd },
|
|
{
|
|
cmd: updateToolCmd,
|
|
options: {
|
|
env: expect.objectContaining({ MISE_SAFE: '1' }),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns null when mise is not allowlisted and the detected version predates safe mode', async () => {
|
|
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
|
|
fs.readLocalFile.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll({
|
|
stdout: '2026.7.11 linux-x64 (2026-07-20)',
|
|
stderr: '',
|
|
});
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
|
|
});
|
|
|
|
it('falls back to conservative behavior when the version probe throws', async () => {
|
|
GlobalConfig.set({ ...adminConfig, allowedUnsafeExecutions: [] });
|
|
fs.readLocalFile.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecSequence([new Error('mise not found')]);
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
// an unparseable/failed probe means safe mode cannot be guaranteed, so the
|
|
// allowlist is still required and locking is skipped
|
|
expect(res).toBeNull();
|
|
expect(execSnapshots).toMatchObject([{ cmd: miseVersionCmd }]);
|
|
});
|
|
|
|
it('does not set MISE_SAFE or probe the version on the allowlisted path', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{
|
|
cmd: updateToolCmd,
|
|
options: {
|
|
env: expect.not.objectContaining({ MISE_SAFE: '1' }),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('returns null if lock file unchanged after exec', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toBeNull();
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateToolCmd },
|
|
]);
|
|
});
|
|
|
|
it('returns updated lock file on success', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
contents: expect.stringContaining('version = "24.16.0"'),
|
|
path: 'mise.lock',
|
|
type: 'addition',
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateToolCmd },
|
|
]);
|
|
});
|
|
|
|
it('returns artifactError on exec failure with combined output', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('existing content');
|
|
const error = new Error('exec error');
|
|
(error as any).stdout = 'stdout output';
|
|
(error as any).stderr = 'stderr output';
|
|
const execSnapshots = mockExecSequence([error]);
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
artifactError: {
|
|
fileName: 'mise.lock',
|
|
stderr: `stdout output\nstderr output\nexec error`,
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([{ cmd: trustCmd }]);
|
|
});
|
|
|
|
it('rethrows TEMPORARY_ERROR', async () => {
|
|
fs.readLocalFile.mockResolvedValueOnce('existing content');
|
|
mockExecSequence([new Error(TEMPORARY_ERROR)]);
|
|
|
|
await expect(
|
|
updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
}),
|
|
).rejects.toThrow(TEMPORARY_ERROR);
|
|
});
|
|
|
|
it('runs mise lock for lockFileMaintenance', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config: lockMaintenanceConfig,
|
|
});
|
|
|
|
// version probe returns nothing here, so no --bump
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: miseVersionCmd },
|
|
{ cmd: trustCmd },
|
|
{ cmd: lockfileMaintenanceCmd },
|
|
]);
|
|
});
|
|
|
|
it('runs mise lock --bump for lockFileMaintenance when mise supports it', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll(safeMiseVersionOutput);
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config: lockMaintenanceConfig,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: miseVersionCmd },
|
|
{ cmd: trustCmd },
|
|
{ cmd: 'mise lock --bump' },
|
|
]);
|
|
});
|
|
|
|
it('runs mise lock <tools> for targeted updates', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }, { depName: 'python' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateMultipleToolsCmd },
|
|
]);
|
|
});
|
|
|
|
it('looks up latest versions for tool versions before `mise lock`, if not set', async () => {
|
|
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
|
|
|
|
for (const version of ['1.2.3', '2.3.4', '3.4.5', '4.5.6', '5.6.7']) {
|
|
datasource.getPkgReleases.mockResolvedValueOnce({
|
|
releases: [{ version: version }],
|
|
});
|
|
}
|
|
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config: {
|
|
// no constraints
|
|
},
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: 'install-tool mise 1.2.3' },
|
|
{ cmd: 'install-tool node 2.3.4' },
|
|
{ cmd: 'install-tool npm 3.4.5' },
|
|
{ cmd: 'install-tool golang 4.5.6' },
|
|
{ cmd: 'install-tool ruby 5.6.7' },
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateToolCmd },
|
|
]);
|
|
});
|
|
|
|
it('uses constraints to specify tool versions before `mise lock`, if set', async () => {
|
|
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config: {
|
|
constraints: {
|
|
mise: '2026.6.12',
|
|
node: '24.16.0',
|
|
npm: '11.4.2',
|
|
go: '1.24.4',
|
|
ruby: '3.4.3',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: 'install-tool mise 2026.6.12' },
|
|
{ cmd: 'install-tool node 24.16.0' },
|
|
{ cmd: 'install-tool npm 11.4.2' },
|
|
{ cmd: 'install-tool golang 1.24.4' },
|
|
{ cmd: 'install-tool ruby 3.4.3' },
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateToolCmd },
|
|
]);
|
|
});
|
|
|
|
it.each`
|
|
depName | oldVersion | newVersion | newPackageFileContent
|
|
${'npm:renovate'} | ${'43.220.0'} | ${'43.233.3'} | ${`[tools]\n"npm:renovate" = "43.233.3"\n`}
|
|
${'go:github.com/DarthSim/hivemind'} | ${'1.0.0'} | ${'1.1.0'} | ${`[tools]\n"go:github.com/DarthSim/hivemind" = { version = "1.1.0", install_env = { GOPROXY = "direct" } }\n`}
|
|
${'gem:rubocop'} | ${'1.75.0'} | ${'1.76.0'} | ${`[tools]\n"gem:rubocop" = "1.76.0"\n`}
|
|
`(
|
|
'updates mise.lock for $depName with dynamic installs and no constraints',
|
|
async ({ depName, oldVersion, newVersion, newPackageFileContent }) => {
|
|
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
|
|
datasource.getPkgReleases.mockResolvedValue({
|
|
releases: [{ version: '1.0.0' }],
|
|
});
|
|
const oldLockFileContent = `[[tools."${depName}"]]\nversion = "${oldVersion}"\nbackend = "${depName}"\n`;
|
|
const newLockFileContent = `[[tools."${depName}"]]\nversion = "${newVersion}"\nbackend = "${depName}"\n`;
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(oldLockFileContent)
|
|
.mockResolvedValueOnce(newLockFileContent);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName }],
|
|
newPackageFileContent,
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: 'mise.lock',
|
|
contents: newLockFileContent,
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: 'install-tool mise 1.0.0' },
|
|
{ cmd: 'install-tool node 1.0.0' },
|
|
{ cmd: 'install-tool npm 1.0.0' },
|
|
{ cmd: 'install-tool golang 1.0.0' },
|
|
{ cmd: 'install-tool ruby 1.0.0' },
|
|
{ cmd: trustCmd },
|
|
{ cmd: `mise lock ${depName}` },
|
|
]);
|
|
},
|
|
);
|
|
|
|
it('injects GITHUB_TOKEN when host rule found', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
hostRules.add({
|
|
hostType: 'github',
|
|
matchHost: 'https://api.github.com/',
|
|
token: 'github-token',
|
|
});
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{
|
|
cmd: updateToolCmd,
|
|
options: {
|
|
cwd: '/tmp/github/some/repo',
|
|
env: expect.objectContaining({
|
|
GITHUB_TOKEN: 'github-token',
|
|
}),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('handles empty updatedDeps with fallback to full lock', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: lockfileMaintenanceCmd },
|
|
]);
|
|
});
|
|
|
|
it('handles environment-specific lock files', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.test.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
contents: expect.stringContaining('version = "24.16.0"'),
|
|
path: 'mise.test.lock',
|
|
type: 'addition',
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustEnvCmd },
|
|
{
|
|
cmd: updateToolCmd,
|
|
options: {
|
|
env: expect.objectContaining({
|
|
MISE_ENV: 'test',
|
|
}),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('uses --local flag for local config files', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.local.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
contents: expect.stringContaining('version = "24.16.0"'),
|
|
path: 'mise.local.lock',
|
|
type: 'addition',
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustLocalCmd },
|
|
{ cmd: 'mise lock --local node' },
|
|
]);
|
|
});
|
|
|
|
it('uses --local flag and MISE_ENV for env-specific local config files', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.test.local.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustEnvLocalCmd },
|
|
{
|
|
cmd: 'mise lock --local node',
|
|
options: {
|
|
env: expect.objectContaining({
|
|
MISE_ENV: 'test',
|
|
}),
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('uses --local flag for lock file maintenance on local config', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.local.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config: lockMaintenanceConfig,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: miseVersionCmd },
|
|
{ cmd: trustLocalCmd },
|
|
{ cmd: 'mise lock --local' },
|
|
]);
|
|
});
|
|
|
|
it('prevents command injection', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce('existing content');
|
|
const execSnapshots = mockExecAll();
|
|
|
|
await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: '|| date; echo ' }, { depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: "mise lock '|| date; echo ' node" },
|
|
]);
|
|
});
|
|
|
|
it('handles subdirectory package files', async () => {
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce('existing content')
|
|
.mockResolvedValueOnce(`[[tools.node]]\nversion = "24.16.0"\n`);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'subdir/mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '',
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
contents: expect.stringContaining('version = "24.16.0"'),
|
|
path: 'subdir/mise.lock',
|
|
type: 'addition',
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustSubdirCmd },
|
|
{
|
|
cmd: updateToolCmd,
|
|
options: {
|
|
cwd: '/tmp/github/some/repo/subdir',
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
describe('updateLockedDependency', () => {
|
|
const lockFileContent = `
|
|
[[tools.node]]
|
|
version = "20.11.0"
|
|
backend = "core:node"
|
|
|
|
[[tools.python]]
|
|
version = "3.10.17"
|
|
`;
|
|
|
|
it('returns already-updated when version matches', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'already-updated' });
|
|
});
|
|
|
|
it('returns already-updated for tool with backend prefix', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'core:node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'already-updated' });
|
|
});
|
|
|
|
it('returns unsupported when version does not match', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '22.0.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns unsupported when tool not in lock file', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'ruby',
|
|
currentVersion: '3.2.0',
|
|
newVersion: '3.3.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns unsupported when no lock file content', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: undefined,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns unsupported for invalid lock file content', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: 'invalid toml {{{',
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns unsupported when depName is undefined', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: undefined as never,
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns update-failed in case of errors', () => {
|
|
vi.spyOn(lockfile, 'getLockedVersion').mockImplementationOnce(() => {
|
|
throw new Error('unexpected error');
|
|
});
|
|
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.11.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'update-failed' });
|
|
});
|
|
});
|
|
});
|