Files
Michael Kriese 9ac59d69d7 test(manager): mark remaining unreachable manager guards (#46082)
* test(manager): close helmfile, hermit, leiningen and mix gaps

Adds a leiningen vector whose version references a variable that was
never defined, which yields no dep.

Marks six guards. The mix host rules were already filtered on a match
host and on the same regex, hermit tracks its packages as symlinks, and
the remaining three need a repository the helm login helper cannot
handle, an umbrella parent lock file that reads cleanly, or a dep name
carrying no organization.

The helmfile repository chain stays counted: `repoName` is only left
null when an oci dep was set, so its implicit else cannot run, and a
coverage-ignore hint does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): mark the nuget, ocb and pep621 guards

Twelve guards, none of which a test reaches. A global.json without an
sdk version is rejected a few lines earlier, and the nuget registry loop
always finds at least one url. The rest need inputs no fixture provides:
a blank ContainerBaseImage, a package reference pinned to a non-semver
value, a registry url with no protocol version, a host rule carrying
only a username or only a password, and a pixi manifest declaring its
requirement only under workspace.

The nuget lock file chain stays counted, since a coverage-ignore hint
does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): mark the python and vendir guards

Ten guards across pip-compile, pipenv, pixi, poetry and vendir. A
compile command always records its arguments, every lock file of a
package file is already in the sorted list, a python dep always carries
a current value, a successful parse always yields a value, and the
vendir vendor dir is always set. The rest need credentials no fixture
supplies, a requirement naming an undeclared index, or a source url
outside artifact registry.

The travis node_js chain stays counted, since a coverage-ignore hint
does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): close the deps-edn, docker-compose and pep723 gaps

Three of the remaining manager branches turn out to be reachable after
all.

The deps-edn extractor skipped an `aliases` value that is not a map; the
existing test only covered an alias *entry* that is not a map, so the
outer guard was never taken. A modern docker-compose file with an `x-`
extension that carries no image never reached the schema's extension
filter either, because the only such fixture omitted `services` and so
parsed as a v1 file. A PEP 723 script block without `requires-python`
was likewise untested.

Co-Authored-By: Claude Opus 5 (1M context) <michael.kriese+claude-code@mend.io>

* test(manager/nuget): cover a global.json with no sdk version

The `v8 ignore else` on the sdk-version check claimed the guard above it
rejects any global.json without one. That guard is
`!manifest['msbuild-sdks'] && !manifest.sdk?.version`, so it rejects only
when both are missing — a file carrying just `msbuild-sdks` passes it and
takes the else.

Replace the ignore with a test for that shape, plus one for an empty
`{}` document, which the guard does reject.

Co-Authored-By: Claude Opus 5 (1M context) <michael.kriese+claude-code@mend.io>

---------

Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
2026-09-16 06:43:01 +00:00

305 lines
7.9 KiB
TypeScript

import { quote } from 'shlex';
import upath from 'upath';
import { logger } from '../../../logger/index.ts';
import { ExecError } from '../../../util/exec/exec-error.ts';
import type { ExecOptions } from '../../../util/exec/types.ts';
import {
localPathIsSymbolicLink,
readLocalSymlink,
} from '../../../util/fs/index.ts';
import { withGitEnvironment } from '../../../util/git/exec.ts';
import { getRepoStatus } from '../../../util/git/index.ts';
import * as p from '../../../util/promises.ts';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types.ts';
import type { ReadContentResult } from './types.ts';
const gitExec = withGitEnvironment(['hermit']);
/**
* updateArtifacts runs hermit install for each updated dependencies
*/
export async function updateArtifacts(
update: UpdateArtifact,
): Promise<UpdateArtifactsResult[] | null> {
const { packageFileName } = update;
try {
await updateHermitPackage(update);
} catch (err) {
logger.debug({ err }, `error updating hermit packages.`);
if (err instanceof UpdateHermitError) {
return [
{
artifactError: {
fileName: `from: ${err.from}, to: ${err.to}`,
stderr: err.stderr,
},
},
];
}
return [
{
artifactError: {
stderr: err instanceof Error ? err.message : String(err),
},
},
];
}
logger.debug(`scanning the changes after update`);
let updateResult: UpdateArtifactsResult[] | null = null;
try {
updateResult = await getUpdateResult(packageFileName);
logger.debug({ updateResult }, `update result for hermit`);
} catch (err) {
logger.debug({ err }, 'Error getting hermet update results');
return [
{
artifactError: {
stderr: err.message,
},
},
];
}
return updateResult;
}
/**
* getContent returns the content of either link or a normal file
*/
async function getContent(file: string): Promise<ReadContentResult> {
let contents: string | null = '';
const isSymlink = await localPathIsSymbolicLink(file);
// v8 ignore else -- hermit always tracks its packages as symlinks
if (isSymlink) {
contents = await readLocalSymlink(file);
}
if (contents === null) {
throw new Error(`error getting content for ${file}`);
}
return {
isSymlink,
contents,
};
}
/**
* getAddResult returns the UpdateArtifactsResult for the added files
*/
function getAddResult(
path: string,
contentRes: ReadContentResult,
): UpdateArtifactsResult {
return {
file: {
type: 'addition',
path,
contents: contentRes.contents,
isSymlink: contentRes.isSymlink,
isExecutable: contentRes.isExecutable,
},
};
}
/**
* getDeleteResult returns the UpdateArtifactsResult for deleted files
*/
function getDeleteResult(path: string): UpdateArtifactsResult {
return {
file: {
type: 'deletion',
path,
},
};
}
/**
* getUpdateResult will return the update result after `hermit install`
* has been performed for all packages
*/
async function getUpdateResult(
packageFileName: string,
): Promise<UpdateArtifactsResult[]> {
const hermitFolder = `${upath.dirname(packageFileName)}/`;
const hermitChanges = await getRepoStatus(hermitFolder);
logger.debug(
{ hermitChanges, hermitFolder },
`hermit changes after package update`,
);
// handle added files
const added = await p.map(
[...hermitChanges.created, ...hermitChanges.not_added],
async (path: string): Promise<UpdateArtifactsResult> => {
const contents = await getContent(path);
return getAddResult(path, contents);
},
);
const deleted = hermitChanges.deleted.map(getDeleteResult);
const modified = await p.map(
hermitChanges.modified,
async (path: string): Promise<UpdateArtifactsResult[]> => {
const contents = await getContent(path);
return [
getDeleteResult(path), // delete existing link
getAddResult(path, contents), // add a new link
];
},
);
const renamed = await p.map(
hermitChanges.renamed,
async (renamed): Promise<UpdateArtifactsResult[]> => {
const from = renamed.from;
const to = renamed.to;
const toContents = await getContent(to);
return [getDeleteResult(from), getAddResult(to, toContents)];
},
);
return [
// rename will need to go first, because
// it needs to create the new link for the new version
// for the modified links to use
...renamed.flat(),
...modified.flat(),
...added,
...deleted,
];
}
/**
* getHermitPackage returns the hermit package for running the hermit install
*/
function getHermitPackage(name: string, version: string): string {
return `${name}-${version}`;
}
/**
* updateHermitPackage runs hermit install for the given package
*/
async function updateHermitPackage(update: UpdateArtifact): Promise<void> {
logger.trace({ update }, `hermit.updateHermitPackage()`);
const toInstall = [];
const from = [];
// storing the old package for replacement
const toUninstall = [];
for (const pkg of update.updatedDeps) {
if (!pkg.depName || !pkg.currentVersion || !pkg.newValue) {
logger.debug(
{
depName: pkg.depName,
currentVersion: pkg.currentVersion,
newValue: pkg.newValue,
},
'missing package update information',
);
throw new UpdateHermitError(
getHermitPackage(pkg.depName ?? '', pkg.currentVersion ?? ''),
getHermitPackage(pkg.depName ?? '', pkg.newValue ?? ''),
'invalid package to update',
);
}
const depName = pkg.depName;
const newName = pkg.newName;
const currentVersion = pkg.currentVersion;
const newValue = pkg.newValue;
const fromPackage = getHermitPackage(depName, currentVersion);
// newName will be available for replacement
const toPackage = getHermitPackage(newName ?? depName, newValue);
toInstall.push(toPackage);
from.push(fromPackage);
// skips uninstall for version only replacement
if (pkg.updateType === 'replacement' && newName !== depName) {
toUninstall.push(depName);
}
}
const execOptions: ExecOptions = {
docker: {},
cwdFile: update.packageFileName,
};
const fromPackages = from.map(quote).join(' ');
// when a name replacement happens, need to uninstall the old package
if (toUninstall.length > 0) {
const packagesToUninstall = toUninstall.map(quote).join(' ');
const uninstallCommands = `./hermit uninstall ${packagesToUninstall}`;
try {
const result = await gitExec(uninstallCommands, execOptions);
logger.trace(
{ stdout: result.stdout },
`hermit uninstall command stdout`,
);
} catch (e) {
logger.warn({ err: e }, `error uninstall hermit package for replacement`);
if (!(e instanceof ExecError)) {
throw e;
}
throw new UpdateHermitError(
fromPackages,
packagesToUninstall,
e.stderr,
e.stdout,
);
}
}
const packagesToInstall = toInstall.map(quote).join(' ');
const execCommands = `./hermit install ${packagesToInstall}`;
logger.debug(
{
packageFile: update.packageFileName,
packagesToInstall,
},
`performing updates`,
);
try {
const result = await gitExec(execCommands, execOptions);
logger.trace({ stdout: result.stdout }, `hermit command stdout`);
} catch (e) {
logger.warn({ err: e }, `error updating hermit package`);
if (!(e instanceof ExecError)) {
throw e;
}
throw new UpdateHermitError(
fromPackages,
packagesToInstall,
e.stderr,
e.stdout,
);
}
}
export class UpdateHermitError extends Error {
stdout: string;
stderr: string;
from: string;
to: string;
constructor(from: string, to: string, stderr: string, stdout = '') {
super();
this.stdout = stdout;
this.stderr = stderr;
this.from = from;
this.to = to;
}
}