import { isUndefined } from '@sindresorhus/is'; import { filterConfig, getManagerConfig, mergeChildConfig, } from '../../../config/index.ts'; import type { RenovateConfig } from '../../../config/types.ts'; import { logger } from '../../../logger/index.ts'; import { getDefaultConfig } from '../../../modules/datasource/index.ts'; import { get } from '../../../modules/manager/index.ts'; import type { PackageFile } from '../../../modules/manager/types.ts'; import { coerceArray } from '../../../util/array.ts'; import { detectSemanticCommits } from '../../../util/git/semantic.ts'; import { applyPackageRules } from '../../../util/package-rules/index.ts'; import { regEx } from '../../../util/regex.ts'; import * as template from '../../../util/template/index.ts'; import { parseUrl } from '../../../util/url.ts'; import type { BranchUpgradeConfig } from '../../types.ts'; import { replacementAlreadyExists } from '../common.ts'; import { generateBranchName } from './branch-name.ts'; function upper(str: string): string { return str.charAt(0).toUpperCase() + str.substring(1); } export function sanitizeDepName(depName: string): string { return depName .replace(regEx(/\$\{[^}]+\}\/?/g), '') .replace(regEx(/[${}]/g), '') .replace('@types/', '') .replace('@', '') .replace(regEx(/\//g), '-') .replace(regEx(/\s+/g), '-') .replace(regEx(/:/g), '-') .replace(regEx(/-+/), '-') .toLowerCase(); } export function applyUpdateConfig(input: BranchUpgradeConfig): any { const updateConfig = { ...input }; delete updateConfig.packageRules; // TODO: Remove next line once #8075 is complete updateConfig.depNameSanitized = updateConfig.depName ? sanitizeDepName(updateConfig.depName) : undefined; updateConfig.newNameSanitized = updateConfig.newName ? sanitizeDepName(updateConfig.newName) : undefined; if (updateConfig.sourceUrl) { const parsedSourceUrl = parseUrl(updateConfig.sourceUrl); if (parsedSourceUrl?.pathname) { updateConfig.sourceRepoSlug = parsedSourceUrl.pathname .replace(regEx(/^\//), '') // remove leading slash .replace(regEx(/\//g), '-') // change slashes to hyphens .replace(regEx(/-+/g), '-'); // remove multiple hyphens updateConfig.sourceRepo = parsedSourceUrl.pathname.replace( regEx(/^\//), '', ); // remove leading slash updateConfig.sourceRepoOrg = updateConfig.sourceRepo.replace( regEx(/\/.*/g), '', ); // remove everything after first slash updateConfig.sourceRepoName = updateConfig.sourceRepo.replace( regEx(/.*\//g), '', ); // remove everything up to the last slash } } if (updateConfig.sourceDirectory) { updateConfig.sourceDirectory = template.compile( updateConfig.sourceDirectory, updateConfig, ); } generateBranchName(updateConfig); return updateConfig; } export async function flattenUpdates( config: RenovateConfig, packageFiles: Record, ): Promise { const updates = []; const updateTypes = [ 'major', 'minor', 'patch', 'pin', 'digest', 'lockFileMaintenance', 'replacement', ]; for (const [manager, files] of Object.entries(packageFiles)) { const managerConfig = getManagerConfig(config, manager); for (const packageFile of files) { const packageFileConfig: BranchUpgradeConfig = mergeChildConfig( managerConfig, packageFile, ) as never; const packagePath = packageFile.packageFile?.split('/'); if (packagePath.length > 0) { packagePath.splice(-1, 1); } if (packagePath.length > 0) { packageFileConfig.parentDir = packagePath.at(-1); packageFileConfig.packageFileDir = packagePath.join('/'); } else { packageFileConfig.parentDir = ''; packageFileConfig.packageFileDir = ''; } let depIndex = 0; for (const dep of packageFile.deps) { if (dep.updates!.length) { const depConfig = mergeChildConfig(packageFileConfig, dep); // @ts-expect-error -- not easily typed delete depConfig.deps; depConfig.depIndex = depIndex; // used for autoreplace for (const update of dep.updates!) { if ( update.updateType === 'replacement' && update.newName && replacementAlreadyExists(packageFile.deps, dep, update.newName) ) { logger.debug( { fileName: packageFile.packageFile }, `Skipping replacement of ${dep.depName} with ${update.newName}: replacement already exists in ${packageFile.packageFile}`, ); continue; } let updateConfig = mergeChildConfig(depConfig, update); delete updateConfig.updates; if (updateConfig.updateType) { // @ts-expect-error -- not easily typed updateConfig[`is${upper(updateConfig.updateType)}`] = true; } if (updateConfig.updateTypes) { updateConfig.updateTypes.forEach((updateType: string) => { // @ts-expect-error -- not easily typed updateConfig[`is${upper(updateType)}`] = true; }); } // apply config from datasource const datasourceConfig = await getDefaultConfig( depConfig.datasource!, ); updateConfig = mergeChildConfig(updateConfig, datasourceConfig); updateConfig = await applyPackageRules( updateConfig, 'datasource-merge', ); // apply major/minor/patch/pin/digest updateConfig = mergeChildConfig( updateConfig, // @ts-expect-error -- not easily typed updateConfig[updateConfig.updateType], ); for (const updateType of updateTypes) { // @ts-expect-error -- not easily typed delete updateConfig[updateType]; } // Apply again in case any were added by the updateType config updateConfig = await applyPackageRules( updateConfig, 'update-type-merge', ); updateConfig = applyUpdateConfig(updateConfig); updateConfig.baseDeps = packageFile.deps; update.branchName = updateConfig.branchName; // make sure that we use the dependency's current state of attestation, rather than using the new update's value updateConfig.hasAttestation = depConfig.hasAttestation; updates.push(updateConfig); } } depIndex += 1; } if ( get(manager, 'supportsLockFileMaintenance') && packageFileConfig.lockFileMaintenance!.enabled ) { // Apply lockFileMaintenance config before packageRules let lockFileConfig = mergeChildConfig( packageFileConfig, packageFileConfig.lockFileMaintenance!, ); lockFileConfig.updateType = 'lockFileMaintenance'; lockFileConfig.isLockFileMaintenance = true; lockFileConfig = await applyPackageRules( lockFileConfig, 'lock-file-maintenance-merge', ); // Apply lockFileMaintenance and packageRules again lockFileConfig = mergeChildConfig( lockFileConfig, lockFileConfig.lockFileMaintenance!, ); lockFileConfig = await applyPackageRules( lockFileConfig, 'lock-file-maintenance-merge-2', ); // Remove unnecessary objects for (const updateType of updateTypes) { // @ts-expect-error -- not easily typed delete lockFileConfig[updateType]; } delete lockFileConfig.packageRules; // @ts-expect-error -- not easily typed delete lockFileConfig.deps; generateBranchName(lockFileConfig); updates.push(lockFileConfig); } if (get(manager, 'updateLockedDependency')) { for (const lockFile of coerceArray(packageFileConfig.lockFiles)) { const lockfileRemediations = config.remediations as Record< string, Record[] >; const remediations = lockfileRemediations?.[lockFile]; if (remediations) { for (const remediation of remediations) { let updateConfig = mergeChildConfig( packageFileConfig, remediation, ); updateConfig = mergeChildConfig( updateConfig, config.vulnerabilityAlerts, ); delete updateConfig.vulnerabilityAlerts; updateConfig.isVulnerabilityAlert = true; updateConfig.isRemediation = true; updateConfig.lockFile = lockFile; updateConfig.currentValue = updateConfig.currentVersion; updateConfig.newValue = updateConfig.newVersion; updateConfig = applyUpdateConfig(updateConfig); updateConfig.enabled = true; updates.push(updateConfig); } } } } } } if (config.semanticCommits === 'auto') { const semanticCommits = await detectSemanticCommits(); for (const update of updates) { update.semanticCommits = semanticCommits; } } const filteredUpdates = updates .filter((update) => update.enabled !== false) .filter((update) => isUndefined(update.skipReason)) .map(({ vulnerabilityAlerts: _, ...update }) => update) .map((update) => filterConfig(update, 'branch')); if (filteredUpdates.length < updates.length) { logger.debug( `Filtered out ${updates.length - filteredUpdates.length} disabled update(s). ${filteredUpdates.length} update(s) remaining.`, ); } return filteredUpdates; }