mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +00:00
ab105e6e42
* feat(gomod): add `gomodTidyAll` option for indirect dependency updates in Go monorepos In Go monorepos it is common for one module to depend on another in the same repo via a local `replace` directive (e.g. `replace example.com/shared => ../shared`). When Renovate updates the shared module, today `go mod tidy` runs only in that module, leaving dependent `go.sum` files stale. Add a new `gomodTidyAll` post-update option that: - Discovers every `go.mod` in the repo and parses its local `replace` directives (both inline and block form). - Builds a dependency graph with `graph-data-structure`, with edges from dependency → dependent, matching the existing NuGet convention. - Runs `go mod tidy` on every transitive dependent in topological order, propagating any `gomodTidy1.17` / `gomodTidyE` flags to the dependent commands. - Collects updated `go.mod` and `go.sum` files from each dependent directory into the artifact result. - Implies `gomodTidy` so users only need to enable one option. Supporting changes: - Extract `getMatchingFiles` and `resolveRelativePathToRoot` into `lib/util/fs/util.ts` so NuGet and gomod can share them instead of each having its own glob/resolve helper. - Refactor `lib/modules/manager/nuget/package-tree.ts` to use the shared `getMatchingFiles` helper and rename local variables for clarity (`deps` → `visited`, `dep` → `dependent`) per review feedback. Docs and config: - Register `gomodTidyAll` in `postUpdateOptions` `allowedValues`. - Add a row for `gomodTidyAll` to the `postUpdateOptions` table in `docs/usage/configuration-options.md`. - Add a "Monorepo tidying for local `replace` directives" subsection to `docs/usage/golang.md` describing when to use the option. Tests and coverage: - `lib/modules/manager/gomod/package-tree.spec.ts` — parse/graph/topo order, 100% line/branch/function coverage. - `lib/modules/manager/gomod/artifacts-gomodtidyall.spec.ts` — six integration cases covering subshell commands, `tidyOpts` propagation, result collection, no-op with no dependents, error handling, and the disabled path. - Full `lib/modules/manager/gomod` suite (151 tests) and NuGet package-tree suite (9 tests) pass; new code is fully covered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(gomod): use named capture groups and direct fs util imports * fix(gomod): use `go -C` for dependent module tidy commands A subshell only works in Docker mode, where all commands are concatenated into a single `bash -l -c`. With `binarySource=install` or `global` each command is split with shlex and run without a shell, so `(cd dir && ...)` is treated as the name of a binary. * refactor(gomod): simplify parsing of local `replace` directives A single regex now covers both the single line form and the lines inside a `replace (...)` block. The previous block pattern stopped at the first `)`, so a block containing a comment with a bracket was skipped, and the single line pattern did not allow a trailing comment. Also keeps the NuGet package tree unchanged apart from the two helpers it now shares. * docs(gomod): warn against combining `gomodTidyAll` with `gomodMassage` Massaging comments out the relative `replace` directives that `gomodTidyAll` follows, so the two options work against each other. * refactor(fs): use `matchRegexOrGlob` in `getMatchingFiles` --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
3.5 KiB
TypeScript
138 lines
3.5 KiB
TypeScript
import { isNonEmptyString } from '@sindresorhus/is';
|
|
import { Graph, hasCycle } from 'graph-data-structure';
|
|
import upath from 'upath';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import {
|
|
getMatchingFiles,
|
|
resolveRelativePathToRoot,
|
|
} from '../../../util/fs/util.ts';
|
|
import { scm } from '../../platform/scm.ts';
|
|
import type { ProjectFile } from './types.ts';
|
|
import { readFileAsXmlDocument } from './util.ts';
|
|
|
|
export const GLOBAL_JSON = 'global.json';
|
|
export const NUGET_CENTRAL_FILE = 'Directory.Packages.props';
|
|
export const MSBUILD_CENTRAL_FILE = 'Packages.props';
|
|
export const DIRECTORY_BUILD_PROPS = 'Directory.Build.props';
|
|
|
|
/**
|
|
* Get all leaf package files of ancestry that depend on packageFileName.
|
|
*/
|
|
export async function getDependentPackageFiles(
|
|
packageFileName: string,
|
|
isPropsFile = false,
|
|
isGlobalJson = false,
|
|
): Promise<ProjectFile[]> {
|
|
const packageFiles = await getAllPackageFiles();
|
|
const graph = new Graph();
|
|
|
|
if (isPropsFile) {
|
|
graph.addNode(packageFileName);
|
|
}
|
|
|
|
if (isGlobalJson) {
|
|
graph.addNode(GLOBAL_JSON);
|
|
}
|
|
|
|
const parentDir =
|
|
packageFileName === NUGET_CENTRAL_FILE ||
|
|
packageFileName === MSBUILD_CENTRAL_FILE ||
|
|
packageFileName === GLOBAL_JSON ||
|
|
packageFileName === DIRECTORY_BUILD_PROPS
|
|
? ''
|
|
: upath.dirname(packageFileName);
|
|
|
|
for (const f of packageFiles) {
|
|
graph.addNode(f);
|
|
|
|
if (
|
|
(isPropsFile || isGlobalJson) &&
|
|
upath.dirname(f).startsWith(parentDir)
|
|
) {
|
|
graph.addEdge(packageFileName, f);
|
|
}
|
|
}
|
|
|
|
for (const f of packageFiles) {
|
|
const doc = await readFileAsXmlDocument(f);
|
|
if (!doc) {
|
|
continue;
|
|
}
|
|
|
|
const projectReferenceAttributes = doc
|
|
.childrenNamed('ItemGroup')
|
|
.map((ig) => ig.childrenNamed('ProjectReference'))
|
|
.flat()
|
|
.map((pf) => pf.attr.Include)
|
|
.filter(isNonEmptyString);
|
|
|
|
const projectReferences = projectReferenceAttributes.map((a) =>
|
|
upath.normalize(a),
|
|
);
|
|
const normalizedRelativeProjectReferences = projectReferences.map((r) =>
|
|
resolveRelativePathToRoot(f, r),
|
|
);
|
|
|
|
for (const ref of normalizedRelativeProjectReferences) {
|
|
graph.addEdge(ref, f);
|
|
}
|
|
|
|
if (hasCycle(graph)) {
|
|
throw new Error('Circular reference detected in NuGet package files');
|
|
}
|
|
}
|
|
|
|
const deps = new Map<string, boolean>();
|
|
recursivelyGetDependentPackageFiles(packageFileName, graph, deps);
|
|
|
|
if (isPropsFile || isGlobalJson) {
|
|
// remove props file, as we don't need it
|
|
deps.delete(packageFileName);
|
|
}
|
|
|
|
// deduplicate
|
|
return Array.from(deps).map(([name, isLeaf]) => ({ name, isLeaf }));
|
|
}
|
|
|
|
/**
|
|
* Traverse graph and find dependent package files at any level of ancestry
|
|
*/
|
|
function recursivelyGetDependentPackageFiles(
|
|
packageFileName: string,
|
|
graph: Graph,
|
|
deps: Map<string, boolean>,
|
|
): void {
|
|
if (deps.has(packageFileName)) {
|
|
// we have already visited this package file
|
|
return;
|
|
}
|
|
|
|
const dependents = graph.adjacent(packageFileName);
|
|
|
|
if (!dependents || dependents.size === 0) {
|
|
deps.set(packageFileName, true);
|
|
return;
|
|
}
|
|
|
|
deps.set(packageFileName, false);
|
|
|
|
for (const dep of dependents) {
|
|
recursivelyGetDependentPackageFiles(dep, graph, deps);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a list of package files in localDir
|
|
*/
|
|
async function getAllPackageFiles(): Promise<string[]> {
|
|
const allFiles = await scm.getFileList();
|
|
const filteredPackageFiles = getMatchingFiles(
|
|
'**/*.{cs,vb,fs}proj',
|
|
allFiles,
|
|
);
|
|
|
|
logger.trace({ filteredPackageFiles }, 'Found package files');
|
|
|
|
return filteredPackageFiles;
|
|
}
|