mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
2e951d2254
* mise: support selector lockfile updates * fix(mise): handle empty lockfile AST * test(mise): use valid empty TOML AST fixture * fix(mise): support current regex utility * fix: clarify lockfile-only lookup handling * fix(mise): handle selector lockfile updates correctly * test(mise): cover selector lockfile review fixes * fix(mise): scope selector artifact refresh * chore: retrigger CI * fix(mise): remove unreachable lockfile guard * refactor(lookup): clarify lockfile-only value handling * refactor(mise): use shared TOML utilities * fix(mise): handle selector lockfile edge cases * fix(mise): satisfy selector CI coverage * fix(mise): require dependency names * fix(mise): preserve configured dependency names * fix(mise): address selector lockfile review feedback * test(mise): cover major selector lockfile updates * chore(static-data): auto-update Java LTS versions So we don't have a hardcoded set of data in the mise manager. Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> * fix(mise): address selector lockfile feedback * test(manager/mise): cover nonnumeric current version prefix formatLockedVersion()'s currentPrefix fallback to '' only runs when the current version has no leading numeric match, which no existing test reached. Co-Authored-By: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io> --------- Co-authored-by: Jamie Tanna <jamie.tanna@mend.io> Co-authored-by: Claude Sonnet 5 <jamie.tanna+claude-code@mend.io>
1037 lines
30 KiB
TypeScript
1037 lines
30 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('refreshes metadata from the updated in-memory lockfile', async () => {
|
|
const originalLockFile =
|
|
'[[tools.node]]\nversion = "20.0.0"\nplatforms = { linux = { checksum = "old" } }\n';
|
|
const updatedLockFile =
|
|
'[[tools.node]]\nversion = "22.0.0"\nplatforms = { linux = { checksum = "new" } }\n';
|
|
const refreshedLockFile =
|
|
'[[tools.node]]\nversion = "22.0.0"\nplatforms = { linux = { checksum = "refreshed" } }\n';
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(originalLockFile)
|
|
.mockResolvedValueOnce(refreshedLockFile);
|
|
const execSnapshots = mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '[tools]\nnode = "22"\n',
|
|
newLockFileContent: updatedLockFile,
|
|
config,
|
|
});
|
|
|
|
expect(fs.writeLocalFile).toHaveBeenCalledWith(
|
|
'mise.lock',
|
|
updatedLockFile,
|
|
);
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: 'mise.lock',
|
|
contents: expect.stringContaining('checksum = "refreshed"'),
|
|
},
|
|
},
|
|
]);
|
|
expect(execSnapshots).toMatchObject([
|
|
{ cmd: trustCmd },
|
|
{ cmd: updateToolCmd },
|
|
]);
|
|
});
|
|
|
|
it('returns regenerated content that matches the in-memory update', async () => {
|
|
const originalLockFile = '[[tools.node]]\nversion = "20.0.0"\n';
|
|
const updatedLockFile = '[[tools.node]]\nversion = "22.0.0"\n';
|
|
fs.readLocalFile
|
|
.mockResolvedValueOnce(originalLockFile)
|
|
.mockResolvedValueOnce(updatedLockFile);
|
|
mockExecAll();
|
|
|
|
const res = await updateArtifacts({
|
|
packageFileName: 'mise.toml',
|
|
updatedDeps: [{ depName: 'node' }],
|
|
newPackageFileContent: '[tools]\nnode = "22"\n',
|
|
newLockFileContent: updatedLockFile,
|
|
config,
|
|
});
|
|
|
|
expect(res).toEqual([
|
|
{
|
|
file: {
|
|
type: 'addition',
|
|
path: 'mise.lock',
|
|
contents: updatedLockFile,
|
|
},
|
|
},
|
|
]);
|
|
});
|
|
|
|
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('falls back to the extracted constraints', 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: {
|
|
extractedConstraints: {
|
|
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('updates the lockfile when version changes', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '22.0.0',
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
status: 'updated',
|
|
files: {
|
|
'mise.lock': expect.stringContaining('version = "22.0.0"'),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('keeps the package file in the update set for artifact refresh', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
packageFileContent: '[[tools.node]]\nversion = "22"\n',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '22.0.0',
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
status: 'updated',
|
|
files: {
|
|
'mise.toml': '[[tools.node]]\nversion = "22"\n',
|
|
'mise.lock': expect.stringContaining('version = "22.0.0"'),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('does not add a null package file to the update set', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
packageFileContent: null as never,
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '22.0.0',
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
status: 'updated',
|
|
files: {
|
|
'mise.lock': expect.stringContaining('version = "22.0.0"'),
|
|
},
|
|
});
|
|
expect(res.files).not.toHaveProperty('mise.toml');
|
|
});
|
|
|
|
it('preserves a vendor prefix in the lockfile version', () => {
|
|
const javaLockFileContent = `
|
|
[[tools.java]]
|
|
version = 'temurin-25.0.3+9.0.LTS'
|
|
backend = 'core:java'
|
|
`;
|
|
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: javaLockFileContent,
|
|
depName: 'java',
|
|
currentVersion: '25.0.3+9.0.LTS',
|
|
newVersion: '25.0.4+8.0.LTS',
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
status: 'updated',
|
|
files: {
|
|
'mise.lock': expect.stringContaining(
|
|
"version = 'temurin-25.0.4+8.0.LTS'",
|
|
),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('supports quoted version keys and ignores other nested keys', () => {
|
|
const quotedKeyLockFileContent = `
|
|
[[tools.node]]
|
|
foo.bar = "ignored"
|
|
"version" = "20.11.0"
|
|
`;
|
|
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: quotedKeyLockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.12.0',
|
|
});
|
|
|
|
expect(res).toMatchObject({
|
|
status: 'updated',
|
|
files: {
|
|
'mise.lock': expect.stringContaining('"version" = "20.12.0"'),
|
|
},
|
|
});
|
|
});
|
|
|
|
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 the lockfile schema is invalid', () => {
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: 'foo = "bar"',
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.12.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns unsupported when the lock entry has no array-table AST node', () => {
|
|
const inlineLockFileContent = `
|
|
tools = { node = [{ version = "20.11.0" }] }
|
|
`;
|
|
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent: inlineLockFileContent,
|
|
depName: 'node',
|
|
currentVersion: '20.10.0',
|
|
newVersion: '20.12.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 unsupported when the lock entry lookup cannot be resolved', () => {
|
|
vi.spyOn(lockfile, 'getLockedVersion').mockReturnValueOnce('20.11.0');
|
|
|
|
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 a prefixed lock entry has no short-name match', () => {
|
|
vi.spyOn(lockfile, 'getLockedVersion').mockReturnValueOnce('20.11.0');
|
|
|
|
const res = updateLockedDependency({
|
|
packageFile: 'mise.toml',
|
|
lockFile: 'mise.lock',
|
|
lockFileContent,
|
|
depName: 'core:ruby',
|
|
currentVersion: '3.2.0',
|
|
newVersion: '3.3.0',
|
|
});
|
|
|
|
expect(res).toEqual({ status: 'unsupported' });
|
|
});
|
|
|
|
it('returns update-failed in case of errors', () => {
|
|
vi.spyOn(lockfile, 'getLockedTool').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' });
|
|
});
|
|
});
|
|
});
|