mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
4220c0fe36
* refactor(manager): share the .npmrc host-rule wrapper between bun, deno and npm Adds `withNpmrcHostRules()` to the npm manager utils: it injects the host-rule lines into a directory's `.npmrc`, runs the callback, and restores the original `.npmrc` from a `finally` block. bun, deno, the corepack `updateArtifacts()` and the three lock file loops in `post-update/index.ts` now share it instead of repeating getNpmrcContent / updateNpmrcContent / resetNpmrcContent. Adds `readUpdatedBinaryLockFile()` to the shared manager utils for the `Buffer.compare` plus file-addition block that bun and deno both carried. Behaviour changes: - bun and deno now restore `.npmrc` when `exec()` throws. The reset previously ran only after a successful install, so a failed lock file update left the injected registry credentials in the working tree. Covered by "restores .npmrc when the install fails" in `bun/artifacts.spec.ts` and `deno/artifacts.spec.ts`. - The npm, yarn and pnpm loops in `post-update/index.ts` now restore `.npmrc` when the loop body throws an `ExternalHostError`, for the same reason. - deno reads `config.isLockFileMaintenance` instead of deriving the flag from `config.updateType === 'lockFileMaintenance'`. Grouped branches overwrite `updateType` while `isLockFileMaintenance` is set explicitly, so the flag is the reliable source. The deno specs were updated to set it. * refactor(manager): let updateLockFile() compare binary lock files A Buffer as the existing content makes the skeleton read, compare and return the new lock file as bytes, so bun and deno use it instead of a separate readUpdatedBinaryLockFile() helper. * refactor(manager/devbox): update the lock file through updateLockFile() devbox kept the same Buffer compare as bun and deno; the trace log before returning the lock file goes away with it. * fix(manager/devbox): type the command list explicitly The list is now only read inside the run() closure, where TypeScript cannot resolve an evolving array type, so tsc reported an implicit any.
229 lines
6.7 KiB
TypeScript
229 lines
6.7 KiB
TypeScript
import { isNonEmptyString } from '@sindresorhus/is';
|
|
import { logger } from '../../logger/index.ts';
|
|
import type { MaybePromise } from '../../types/index.ts';
|
|
import { detectPlatform } from '../../util/common.ts';
|
|
import type { ExecError } from '../../util/exec/exec-error.ts';
|
|
import type { ConstraintName } from '../../util/exec/types.ts';
|
|
import {
|
|
deleteLocalFile,
|
|
readLocalFile,
|
|
writeLocalFile,
|
|
} from '../../util/fs/index.ts';
|
|
import type { FileChange } from '../../util/git/types.ts';
|
|
import { parseGitUrl } from '../../util/git/url.ts';
|
|
import { GitRefsDatasource } from '../datasource/git-refs/index.ts';
|
|
import { GitTagsDatasource } from '../datasource/git-tags/index.ts';
|
|
import { GithubTagsDatasource } from '../datasource/github-tags/index.ts';
|
|
import { GitlabTagsDatasource } from '../datasource/gitlab-tags/index.ts';
|
|
import type {
|
|
PackageDependency,
|
|
ToolConstraintsConfig,
|
|
UpdateArtifactsResult,
|
|
UpdateLockFileConfig,
|
|
} from './types.ts';
|
|
|
|
export function applyGitSource(
|
|
dep: PackageDependency,
|
|
git: string,
|
|
rev: string | undefined,
|
|
tag: string | undefined,
|
|
branch: string | undefined,
|
|
): void {
|
|
if (tag) {
|
|
const platform = detectPlatform(git);
|
|
if (platform === 'github' || platform === 'gitlab') {
|
|
dep.datasource =
|
|
platform === 'github'
|
|
? GithubTagsDatasource.id
|
|
: GitlabTagsDatasource.id;
|
|
const { host, full_name } = parseGitUrl(git);
|
|
|
|
// Always use HTTPS for GitHub/GitLab API endpoints, even if the git URL protocol is SSH.
|
|
dep.registryUrls = [`https://${host}`];
|
|
dep.packageName = full_name;
|
|
} else {
|
|
dep.datasource = GitTagsDatasource.id;
|
|
dep.packageName = git;
|
|
}
|
|
dep.currentValue = tag;
|
|
dep.skipReason = undefined;
|
|
} else if (rev) {
|
|
dep.datasource = GitRefsDatasource.id;
|
|
dep.packageName = git;
|
|
dep.currentDigest = rev;
|
|
dep.replaceString = rev;
|
|
dep.skipReason = undefined;
|
|
} else {
|
|
dep.datasource = GitRefsDatasource.id;
|
|
dep.packageName = git;
|
|
dep.currentValue = branch;
|
|
dep.skipReason = branch ? 'git-dependency' : 'unspecified-version';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given an {@link ExecError}, retrieve the message which will be used for an {@link ArtifactError}.
|
|
*
|
|
* An `ExecError` always carries a `stderr` property, so nullish coalescing would keep an empty string and render an artifact error with no message at all.
|
|
*
|
|
*/
|
|
export function artifactErrorMessageFromExecError(
|
|
err: Partial<ExecError>,
|
|
message: string,
|
|
): string {
|
|
if (err.stderr?.trim()) {
|
|
return err.stderr;
|
|
}
|
|
|
|
if (err.stdout?.trim()) {
|
|
return err.stdout;
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/**
|
|
* Wraps {@link FileChange}s, e.g. the result of `collectFileChanges()`, into the
|
|
* result shape returned by `updateArtifacts()`.
|
|
*/
|
|
export function fileChangesToArtifactResults(
|
|
changes: FileChange[],
|
|
): UpdateArtifactsResult[] {
|
|
return changes.map((file) => ({ file }));
|
|
}
|
|
|
|
/**
|
|
* Resolve the constraint for a tool that `updateArtifacts()` has to run.
|
|
*
|
|
* The precedence is:
|
|
*
|
|
* 1. `constraints`, because the user asked for it explicitly
|
|
* 2. `derive`, because it reads the package files as the branch changed them,
|
|
* so it can be newer than what extraction saw
|
|
* 3. `extractedConstraints`, as collected while extracting the base branch
|
|
*
|
|
* Managers that have no way to derive the constraint at artifact time can omit
|
|
* `derive`. An empty string counts as "not set" at every step, so it never
|
|
* shadows a value further down the list.
|
|
*
|
|
* This is the only place that reads `constraints` and `extractedConstraints`
|
|
* from the config; the `renovate/prefer-resolve-tool-constraint` lint rule
|
|
* keeps managers from reading them directly.
|
|
*/
|
|
export async function resolveToolConstraint(
|
|
config: ToolConstraintsConfig,
|
|
toolName: ConstraintName,
|
|
derive?: () => MaybePromise<string | null | undefined>,
|
|
): Promise<string | undefined> {
|
|
const configured = config.constraints?.[toolName];
|
|
if (isNonEmptyString(configured)) {
|
|
return configured;
|
|
}
|
|
const derived = await derive?.();
|
|
if (isNonEmptyString(derived)) {
|
|
return derived;
|
|
}
|
|
const extracted = config.extractedConstraints?.[toolName];
|
|
return isNonEmptyString(extracted) ? extracted : undefined;
|
|
}
|
|
|
|
/**
|
|
* The result which reports `path` as created or updated.
|
|
*/
|
|
export function fileAddition(
|
|
path: string,
|
|
contents: string | Buffer | null,
|
|
): UpdateArtifactsResult {
|
|
return { file: { type: 'addition', path, contents } };
|
|
}
|
|
|
|
/**
|
|
* The result which reports a failed artifact update to the user.
|
|
*/
|
|
export function artifactError(
|
|
fileName: string | undefined,
|
|
stderr: string,
|
|
): UpdateArtifactsResult {
|
|
return { artifactError: { fileName, stderr } };
|
|
}
|
|
|
|
/**
|
|
* The result for an artifact update which threw, using the most informative
|
|
* output the error carries. Callers are expected to have rethrown
|
|
* `TEMPORARY_ERROR` and logged the error before calling this.
|
|
*/
|
|
export function artifactErrorResult(
|
|
fileName: string | undefined,
|
|
err: Error & Partial<ExecError>,
|
|
): UpdateArtifactsResult[] {
|
|
return [
|
|
artifactError(
|
|
fileName,
|
|
artifactErrorMessageFromExecError(err, err.message),
|
|
),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* The skeleton shared by the managers which regenerate a single lock file:
|
|
* rewrite the package file, optionally drop the lock file, run the package
|
|
* manager and return the lock file when its content changed.
|
|
*
|
|
* Errors from `run()` are not handled here - callers keep their own logging and
|
|
* pass the error to {@link artifactErrorResult}.
|
|
*/
|
|
export async function updateLockFile({
|
|
lockFileName,
|
|
existingLockFileContent,
|
|
packageFile,
|
|
deleteLockFile,
|
|
run,
|
|
}: UpdateLockFileConfig): Promise<UpdateArtifactsResult[] | null> {
|
|
if (packageFile) {
|
|
await writeLocalFile(packageFile.path, packageFile.contents);
|
|
}
|
|
|
|
if (deleteLockFile) {
|
|
await deleteLocalFile(lockFileName);
|
|
}
|
|
|
|
await run();
|
|
|
|
// A Buffer as the existing content means the lock file may be binary, so the
|
|
// new content is read and compared as bytes as well.
|
|
const newLockFileContent = await readLockFile(
|
|
lockFileName,
|
|
Buffer.isBuffer(existingLockFileContent),
|
|
);
|
|
if (!newLockFileContent) {
|
|
logger.debug(`No ${lockFileName} found`);
|
|
return null;
|
|
}
|
|
|
|
if (isSameContent(existingLockFileContent, newLockFileContent)) {
|
|
logger.debug(`${lockFileName} is unchanged`);
|
|
return null;
|
|
}
|
|
|
|
return [fileAddition(lockFileName, newLockFileContent)];
|
|
}
|
|
|
|
function readLockFile(
|
|
lockFileName: string,
|
|
asBuffer: boolean,
|
|
): Promise<string | Buffer | null> {
|
|
return asBuffer
|
|
? readLocalFile(lockFileName)
|
|
: readLocalFile(lockFileName, 'utf8');
|
|
}
|
|
|
|
function isSameContent(
|
|
before: string | Buffer | null,
|
|
after: string | Buffer,
|
|
): boolean {
|
|
if (Buffer.isBuffer(before) && Buffer.isBuffer(after)) {
|
|
return before.equals(after);
|
|
}
|
|
return before === after;
|
|
}
|