mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +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>
175 lines
4.7 KiB
TypeScript
175 lines
4.7 KiB
TypeScript
import upath from 'upath';
|
|
import { GlobalConfig } from '../../config/global.ts';
|
|
import { FILE_ACCESS_VIOLATION_ERROR } from '../../constants/error-messages.ts';
|
|
import {
|
|
ensureCachePath,
|
|
ensureLocalPath,
|
|
getMatchingFiles,
|
|
isValidPath,
|
|
resolveRelativePathToRoot,
|
|
} from './util.ts';
|
|
|
|
describe('util/fs/util', () => {
|
|
const localDir = upath.resolve('/foo');
|
|
const cacheDir = upath.resolve('/bar');
|
|
|
|
beforeAll(() => {
|
|
GlobalConfig.set({ localDir, cacheDir });
|
|
});
|
|
|
|
it.each`
|
|
path | fullPath
|
|
${''} | ${`${localDir}`}
|
|
${'baz'} | ${`${localDir}/baz`}
|
|
`(`ensureLocalPath('$path', '$fullPath')`, ({ path, fullPath }) => {
|
|
expect(ensureLocalPath(path)).toBe(fullPath);
|
|
});
|
|
|
|
it.each`
|
|
path
|
|
${'..'}
|
|
${'../etc/passwd'}
|
|
${'/foo/../bar'}
|
|
${'/foo/../../etc/passwd'}
|
|
${'/baz'}
|
|
`(`ensureLocalPath('$path', '${localDir}') - throws`, ({ path }) => {
|
|
expect(() => ensureLocalPath(path)).toThrow(FILE_ACCESS_VIOLATION_ERROR);
|
|
});
|
|
|
|
it.each`
|
|
path | fullPath
|
|
${''} | ${`${cacheDir}`}
|
|
${'baz'} | ${`${cacheDir}/baz`}
|
|
`(`ensureCachePath('$path', '$fullPath')`, ({ path, fullPath }) => {
|
|
expect(ensureCachePath(path)).toBe(fullPath);
|
|
});
|
|
|
|
it.each`
|
|
path
|
|
${'..'}
|
|
${'../etc/passwd'}
|
|
${'/bar/../foo'}
|
|
${'/bar/../../etc/passwd'}
|
|
${'/baz'}
|
|
${'/baz"'}
|
|
`(`ensureCachePath('$path', '${cacheDir}') - throws`, ({ path }) => {
|
|
expect(() => ensureCachePath(path)).toThrow(FILE_ACCESS_VIOLATION_ERROR);
|
|
});
|
|
|
|
it.each`
|
|
value | expected
|
|
${'.'} | ${true}
|
|
${'./...'} | ${true}
|
|
${'foo'} | ${true}
|
|
${'foo/bar'} | ${true}
|
|
${'./foo/bar'} | ${true}
|
|
${'./foo/bar/...'} | ${true}
|
|
${'..'} | ${false}
|
|
${'....'} | ${true}
|
|
${'./foo/..'} | ${true}
|
|
${'./foo/..../bar'} | ${true}
|
|
${'./..'} | ${false}
|
|
${'\\foo'} | ${false}
|
|
${"foo'"} | ${true}
|
|
${'fo"o'} | ${true}
|
|
${'fo&o'} | ${true}
|
|
${'f;oo'} | ${true}
|
|
${'f o o'} | ${true}
|
|
${'/'} | ${false}
|
|
${'/foo'} | ${false}
|
|
${'&&'} | ${true}
|
|
${';'} | ${true}
|
|
${'./[foo]/bar'} | ${true}
|
|
`('isValidPath($value) == $expected', ({ value, expected }) => {
|
|
expect(isValidPath(value, 'cacheDir')).toBe(expected);
|
|
});
|
|
|
|
describe('resolveRelativePathToRoot', () => {
|
|
it('resolves relative path from a subdirectory file', () => {
|
|
const result = resolveRelativePathToRoot(
|
|
'two/two.csproj',
|
|
'../one/one.csproj',
|
|
);
|
|
expect(result).toBe('one/one.csproj');
|
|
});
|
|
|
|
it('resolves same-directory reference', () => {
|
|
const result = resolveRelativePathToRoot(
|
|
'moduleA/go.mod',
|
|
'./local/go.mod',
|
|
);
|
|
expect(result).toBe('moduleA/local/go.mod');
|
|
});
|
|
|
|
it('resolves parent directory reference', () => {
|
|
const result = resolveRelativePathToRoot(
|
|
'services/api/go.mod',
|
|
'../../libs/shared',
|
|
);
|
|
expect(result).toBe('libs/shared');
|
|
});
|
|
|
|
it('resolves from root-level file', () => {
|
|
const result = resolveRelativePathToRoot('go.mod', './sub');
|
|
expect(result).toBe('sub');
|
|
});
|
|
|
|
it('resolves deeply nested paths', () => {
|
|
const result = resolveRelativePathToRoot(
|
|
'a/b/c/file.txt',
|
|
'../../../other/file.txt',
|
|
);
|
|
expect(result).toBe('other/file.txt');
|
|
});
|
|
});
|
|
|
|
describe('getMatchingFiles', () => {
|
|
const allFiles = [
|
|
'one/one.csproj',
|
|
'two/two.vbproj',
|
|
'three/three.fsproj',
|
|
'readme.md',
|
|
'go.mod',
|
|
'api/go.mod',
|
|
'cmd/go.mod',
|
|
'go.sum',
|
|
'package.json',
|
|
];
|
|
|
|
it('filters files using glob pattern', () => {
|
|
expect(getMatchingFiles('**/*.{cs,vb,fs}proj', allFiles)).toEqual([
|
|
'one/one.csproj',
|
|
'two/two.vbproj',
|
|
'three/three.fsproj',
|
|
]);
|
|
});
|
|
|
|
it('filters go.mod files', () => {
|
|
expect(getMatchingFiles('**/go.mod', allFiles)).toEqual([
|
|
'go.mod',
|
|
'api/go.mod',
|
|
'cmd/go.mod',
|
|
]);
|
|
});
|
|
|
|
it('does not match files with a matching suffix only', () => {
|
|
expect(
|
|
getMatchingFiles('**/go.mod', ['rego.mod', 'sub/rego.mod']),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('filters files using regex pattern', () => {
|
|
expect(getMatchingFiles('/^.*\\/go\\.mod$/', allFiles)).toEqual([
|
|
'api/go.mod',
|
|
'cmd/go.mod',
|
|
]);
|
|
});
|
|
|
|
it('returns empty array when no files match', () => {
|
|
expect(
|
|
getMatchingFiles('**/go.mod', ['readme.md', 'package.json']),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
});
|