Files
renovate/lib/modules/manager/gomod/artifacts-extra.spec.ts
Jamie Tanna 9ecbaa8581 chore(manager/gomod): clarify how artifact updates work with minimumReleaseAge (#45526)
When Go updates dependencies using i.e. `go get` or `go mod download`,
it uses Minimal Version Selection (MVS)[0], which takes the lowest
version available from the given packages.

In the case of an updated dependency, in practice this means that Go
will chose the version of an indirect (transitive) package dependency as
the version that the updated package points to in their `go.mod`, which
must be older than the updated package, as the updated package has only
just been updated.

This protects against cases in other ecosystems where an unpinned
transitive dependency can be pulled by an updated package, potentially
bypassing `minimumReleaseAge`.

However, I can imagine there may be some questions about this, so we can
preemptively add a note into the `Artifact update notice` when
`minimumReleaseAge` is set.

[0]: https://go.dev/ref/mod#minimal-version-selection
2026-08-28 11:39:19 +00:00

371 lines
11 KiB
TypeScript

import { codeBlock } from 'common-tags';
import {
extraDepsTable,
getExtraDeps,
getExtraDepsNotice,
} from './artifacts-extra.ts';
import type { ExtraDep } from './types.ts';
describe('modules/manager/gomod/artifacts-extra', () => {
const goModBefore = codeBlock`
go 1.22.0
require (
github.com/foo/foo v1.0.0
github.com/bar/bar v2.0.0
)
replace baz/baz => qux/qux
`;
const goModAfter = codeBlock`
go 1.22.2
// Note the order change
require (
github.com/bar/bar v2.2.2
github.com/foo/foo v1.1.1
)
replace baz/baz => quux/quux
`;
describe('getExtraDeps', () => {
it('detects extra dependencies', () => {
const excludeDeps = ['github.com/foo/foo'];
const res = getExtraDeps(goModBefore, goModAfter, excludeDeps);
expect(res).toEqual([
{
depName: 'go',
currentValue: '1.22.0',
newValue: '1.22.2',
},
{
depName: 'github.com/bar/bar',
currentValue: 'v2.0.0',
newValue: 'v2.2.2',
},
] satisfies ExtraDep[]);
});
});
describe('extraDepsTable', () => {
it('generates a table', () => {
const extraDeps: ExtraDep[] = [
{
depName: 'github.com/foo/foo',
currentValue: 'v1.0.0',
newValue: 'v1.1.1',
},
{
depName: 'github.com/bar/bar',
currentValue: 'v2.0.0',
newValue: 'v2.2.2',
},
];
const res = extraDepsTable(extraDeps);
expect(res).toEqual(
[
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `github.com/foo/foo` | `v1.0.0` -> `v1.1.1` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
});
describe('getExtraDepsNotice', () => {
it('returns null when one of files is missing', () => {
expect(getExtraDepsNotice(null, goModAfter, [], {})).toBeNull();
expect(getExtraDepsNotice(goModBefore, null, [], {})).toBeNull();
});
it('returns null when all dependencies are excluded', () => {
const excludeDeps = ['go', 'github.com/foo/foo', 'github.com/bar/bar'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {});
expect(res).toBeNull();
});
it('returns a notice when there is an extra dependency', () => {
const excludeDeps = ['go', 'github.com/foo/foo'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {});
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 1 additional dependency was updated',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('returns a notice when there are extra dependencies', () => {
const excludeDeps = ['go'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {});
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 2 additional dependencies were updated',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `github.com/foo/foo` | `v1.0.0` -> `v1.1.1` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('adds special notice for updated `go` version', () => {
const excludeDeps = ['github.com/foo/foo'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {});
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 1 additional dependency was updated',
'- The `go` directive was updated for compatibility reasons',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `go` | `1.22.0` -> `1.22.2` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('correctly identifies toolchain updates vs go version updates', () => {
const toolChainUpdategoModBefore = codeBlock`
go 1.22.0
toolchain go1.23.0
require (
github.com/foo/foo v1.0.0
github.com/bar/bar v2.0.0
)
`;
const toolChainUpdategoModAfter = codeBlock`
go 1.22.0
toolchain go1.24.0
// Note the order change
require (
github.com/bar/bar v2.2.2
github.com/foo/foo v1.1.1
)
`;
const res = getExtraDepsNotice(
toolChainUpdategoModBefore,
toolChainUpdategoModAfter,
[],
{},
);
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 2 additional dependencies were updated',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `go (toolchain)` | `1.23.0` -> `1.24.0` |',
'| `github.com/foo/foo` | `v1.0.0` -> `v1.1.1` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('correctly identifies and distinguishes toolchain updates vs go version updates when both are present', () => {
const toolChainUpdategoModBefore = codeBlock`
go 1.22.0
toolchain go1.23.0
require (
github.com/foo/foo v1.0.0
github.com/bar/bar v2.0.0
)
`;
const toolChainUpdategoModAfter = codeBlock`
go 1.22.2
toolchain go1.24.0
// Note the order change
require (
github.com/bar/bar v2.2.2
github.com/foo/foo v1.1.1
)
`;
const res = getExtraDepsNotice(
toolChainUpdategoModBefore,
toolChainUpdategoModAfter,
[],
{},
);
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 2 additional dependencies were updated',
'- The `go` directive was updated for compatibility reasons',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `go` | `1.22.0` -> `1.22.2` |',
'| `go (toolchain)` | `1.23.0` -> `1.24.0` |',
'| `github.com/foo/foo` | `v1.0.0` -> `v1.1.1` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('correctly handles the introduction of a toolchain directive by not indicating a change', () => {
const toolChainUpdategoModBefore = codeBlock`
go 1.22.0
require (
github.com/foo/foo v1.0.0
github.com/bar/bar v2.0.0
)
`;
const toolChainUpdategoModAfter = codeBlock`
go 1.22.0
toolchain go1.24.0
// Note the order change
require (
github.com/bar/bar v2.2.2
github.com/foo/foo v1.1.1
)
`;
const res = getExtraDepsNotice(
toolChainUpdategoModBefore,
toolChainUpdategoModAfter,
[],
{},
);
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 2 additional dependencies were updated',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `github.com/foo/foo` | `v1.0.0` -> `v1.1.1` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('indicates that there are no Minimum Release Age concerns, if `minimumReleaseAge` is set', () => {
const excludeDeps = ['github.com/foo/foo'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {
minimumReleaseAge: '2 days',
});
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 1 additional dependency was updated',
'- The `go` directive was updated for compatibility reasons',
'',
'',
"Due to Go's usage of [Minimal Version Selection (MVS)](https://go.dev/ref/mod#minimal-version-selection), these packages have been updated to the minimum version available, so will still abide by `minimumReleaseAge=2 days`",
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `go` | `1.22.0` -> `1.22.2` |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
it('does not indicates that there are no Minimum Release Age concerns, if `minimumReleaseAge=null`', () => {
const excludeDeps = ['go', 'github.com/foo/foo'];
const res = getExtraDepsNotice(goModBefore, goModAfter, excludeDeps, {
minimumReleaseAge: null,
});
expect(res).toEqual(
[
'In order to perform the update(s) described in the table above, Renovate ran the `go get` command, which resulted in the following additional change(s):',
'',
'',
'- 1 additional dependency was updated',
'',
'',
'Details:',
'',
'',
'| **Package** | **Change** |',
'| :------------------- | :------------------- |',
'| `github.com/bar/bar` | `v2.0.0` -> `v2.2.2` |',
].join('\n'),
);
});
});
});