mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
8a378fc5b4
For exact (single-version) specs, only update a pin whose current version matches the version being upgraded from. This prevents the repo-wide Package.resolved sweep from rewriting a same-named transitive pin in an unrelated package to a version that package's constraints don't allow. Co-authored-by: Jamie Tanna <jamie.tanna@mend.io>
284 lines
8.1 KiB
TypeScript
284 lines
8.1 KiB
TypeScript
import { logger } from '../../../logger/index.ts';
|
|
import { readLocalFile } from '../../../util/fs/index.ts';
|
|
import { parseGitUrl } from '../../../util/git/url.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { trimTrailingSlash } from '../../../util/url.ts';
|
|
import { GitTagsDatasource } from '../../datasource/git-tags/index.ts';
|
|
import { getDigest } from '../../datasource/index.ts';
|
|
import { scm } from '../../platform/scm.ts';
|
|
import { api as swiftVersioning } from '../../versioning/swift/index.ts';
|
|
import type {
|
|
PackageDependency,
|
|
UpdateArtifact,
|
|
UpdateArtifactsResult,
|
|
Upgrade,
|
|
} from '../types.ts';
|
|
import type { PackageResolvedPin } from './schema.ts';
|
|
import { PackageResolvedJson } from './schema.ts';
|
|
|
|
async function findPackageResolvedFiles(): Promise<string[]> {
|
|
const fileList = await scm.getFileList();
|
|
return fileList.filter((f) => f.endsWith('Package.resolved'));
|
|
}
|
|
|
|
function normalizeUrl(url: string): string {
|
|
try {
|
|
const parsed = parseGitUrl(url);
|
|
// Only use parsed result when both resource and full_name are present,
|
|
// otherwise fall through to basic normalization
|
|
if (parsed.resource && parsed.full_name) {
|
|
// Normalize to host/full_name form to allow cross-protocol matching
|
|
// e.g. https://github.com/owner/repo, git@github.com:owner/repo.git
|
|
// -> github.com/owner/repo
|
|
return `${parsed.resource}/${trimTrailingSlash(parsed.full_name)}`.toLowerCase();
|
|
}
|
|
} catch {
|
|
// Fall through to basic normalization
|
|
}
|
|
return url
|
|
.replace(regEx(/\.git$/), '')
|
|
.replace(regEx(/\/$/), '')
|
|
.toLowerCase();
|
|
}
|
|
|
|
function matchPinForDep(
|
|
dep: PackageDependency,
|
|
pins: PackageResolvedPin[],
|
|
): PackageResolvedPin | null {
|
|
let depUrl: string;
|
|
|
|
if (dep.datasource === GitTagsDatasource.id) {
|
|
/* v8 ignore next -- extract always sets depName for a git-tags dep */
|
|
depUrl = dep.depName ?? '';
|
|
} else {
|
|
const registryUrl = dep.registryUrls?.[0] ?? 'https://github.com';
|
|
depUrl = `${registryUrl}/${dep.depName}`;
|
|
}
|
|
|
|
const normalizedDepUrl = normalizeUrl(depUrl);
|
|
|
|
return (
|
|
pins.find((pin) => normalizeUrl(pin.location) === normalizedDepUrl) ?? null
|
|
);
|
|
}
|
|
|
|
async function resolveCommitSha(
|
|
dep: Upgrade,
|
|
newVersion: string,
|
|
): Promise<string | null> {
|
|
if (dep.newDigest) {
|
|
return dep.newDigest;
|
|
}
|
|
|
|
const datasource = dep.datasource;
|
|
const packageName = dep.packageName ?? dep.depName;
|
|
if (!datasource || !packageName) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const digest = await getDigest(
|
|
{
|
|
datasource,
|
|
packageName,
|
|
registryUrls: dep.registryUrls,
|
|
},
|
|
newVersion,
|
|
);
|
|
return digest;
|
|
} catch (err) {
|
|
logger.debug(
|
|
{ err, packageName, newVersion },
|
|
'swift: failed to resolve commit SHA',
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function updatePinInJson(
|
|
content: string,
|
|
pin: PackageResolvedPin,
|
|
newVersion: string,
|
|
newRevision: string | null,
|
|
): string {
|
|
let updated = content;
|
|
|
|
// Find the pin block by its unique identity value
|
|
const identityPattern = regEx(
|
|
`"identity"\\s*:\\s*"${RegExp.escape(pin.identity)}"`,
|
|
);
|
|
const identityMatch = identityPattern.exec(updated);
|
|
/* istanbul ignore if: identity always exists after JSON parse + matchPinForDep */
|
|
if (!identityMatch) {
|
|
return content;
|
|
}
|
|
|
|
// Find the enclosing object block (search for the opening brace before identity)
|
|
const beforeIdentity = updated.slice(0, identityMatch.index);
|
|
const blockStart = beforeIdentity.lastIndexOf('{');
|
|
/* istanbul ignore if: valid JSON always has enclosing braces */
|
|
if (blockStart === -1) {
|
|
return content;
|
|
}
|
|
|
|
// Find the closing brace of this pin block (handle nested braces)
|
|
let braceDepth = 0;
|
|
let blockEnd = -1;
|
|
for (let i = blockStart; i < updated.length; i++) {
|
|
if (updated[i] === '{') {
|
|
braceDepth++;
|
|
} else if (updated[i] === '}') {
|
|
braceDepth--;
|
|
if (braceDepth === 0) {
|
|
blockEnd = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/* istanbul ignore if: valid JSON always has matching braces */
|
|
if (blockEnd === -1) {
|
|
return content;
|
|
}
|
|
|
|
let pinBlock = updated.slice(blockStart, blockEnd);
|
|
|
|
// Replace version within the pin block
|
|
const versionPattern = regEx(/(?<prefix>"version"\s*:\s*)"[^"]*"/);
|
|
pinBlock = pinBlock.replace(versionPattern, `$<prefix>"${newVersion}"`);
|
|
|
|
// Replace revision within the pin block if we have a new one
|
|
if (newRevision) {
|
|
const revisionPattern = regEx(/(?<prefix>"revision"\s*:\s*)"[^"]*"/);
|
|
pinBlock = pinBlock.replace(revisionPattern, `$<prefix>"${newRevision}"`);
|
|
}
|
|
|
|
updated = updated.slice(0, blockStart) + pinBlock + updated.slice(blockEnd);
|
|
return updated;
|
|
}
|
|
|
|
export async function updateArtifacts({
|
|
updatedDeps,
|
|
config,
|
|
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
|
|
if (config.isLockFileMaintenance) {
|
|
logger.debug('swift: lockFileMaintenance is not supported');
|
|
return null;
|
|
}
|
|
|
|
if (!updatedDeps.length) {
|
|
logger.debug('swift: no updatedDeps, nothing to do');
|
|
return null;
|
|
}
|
|
|
|
const resolvedFiles = await findPackageResolvedFiles();
|
|
if (!resolvedFiles.length) {
|
|
logger.debug('swift: no Package.resolved files found');
|
|
return null;
|
|
}
|
|
|
|
const results: UpdateArtifactsResult[] = [];
|
|
|
|
for (const resolvedFile of resolvedFiles) {
|
|
const content = await readLocalFile(resolvedFile, 'utf8');
|
|
if (!content) {
|
|
logger.debug({ resolvedFile }, 'swift: could not read Package.resolved');
|
|
continue;
|
|
}
|
|
|
|
const parseResult = PackageResolvedJson.safeParse(content);
|
|
if (!parseResult.success) {
|
|
logger.debug(
|
|
{ resolvedFile, err: parseResult.error },
|
|
'swift: could not parse Package.resolved',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const parsed = parseResult.data;
|
|
let updated = content;
|
|
|
|
for (const dep of updatedDeps) {
|
|
const newValue = dep.newValue;
|
|
if (!newValue) {
|
|
continue;
|
|
}
|
|
|
|
// shouldn't happen
|
|
if (!dep.newVersion) {
|
|
logger.debug(
|
|
{
|
|
depName: dep.depName,
|
|
newVersion: dep.newVersion,
|
|
newValue: dep.newValue,
|
|
},
|
|
'swift: found a newValue but not a newVersion',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const pin = matchPinForDep(dep, parsed.pins);
|
|
if (!pin) {
|
|
logger.debug(
|
|
{ depName: dep.depName },
|
|
'swift: no matching pin found in Package.resolved',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Only rewrite a pin we can confidently attribute to the dependency being
|
|
// upgraded. For exact (single-version) specs the pin to update is the one
|
|
// currently at the from-version; a pin at any other version belongs to a
|
|
// different constraint — e.g. a transitive dependency in an unrelated
|
|
// package — and must be left untouched, otherwise we may write a version
|
|
// that package does not allow.
|
|
// https://github.com/renovatebot/renovate/issues/44312
|
|
if (
|
|
dep.currentValue &&
|
|
swiftVersioning.isSingleVersion(dep.currentValue)
|
|
) {
|
|
const fromVersion = (dep.currentVersion ?? dep.currentValue).replace(
|
|
regEx(/^v/),
|
|
'',
|
|
);
|
|
if (pin.state.version !== fromVersion) {
|
|
logger.debug(
|
|
{
|
|
depName: dep.depName,
|
|
pinVersion: pin.state.version,
|
|
currentValue: dep.currentValue,
|
|
},
|
|
'swift: pin does not match the upgraded-from version; skipping to avoid touching an unrelated pin',
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const resolvedVersion = dep.newVersion.replace(regEx(/^v/), '');
|
|
|
|
// Skip if already up-to-date
|
|
if (pin.state.version === resolvedVersion) {
|
|
logger.debug(
|
|
{ depName: dep.depName, resolvedVersion },
|
|
'swift: pin already at target version',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const newRevision = await resolveCommitSha(dep, dep.newVersion);
|
|
updated = updatePinInJson(updated, pin, resolvedVersion, newRevision);
|
|
}
|
|
|
|
if (updated !== content) {
|
|
results.push({
|
|
file: {
|
|
type: 'addition',
|
|
path: resolvedFile,
|
|
contents: updated,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
return results.length ? results : null;
|
|
}
|