From 91812d025da748722777156da936d07e205cf0d9 Mon Sep 17 00:00:00 2001
From: Jorge Silva <jorgesilva.me@gmail.com>
Date: Tue, 18 Apr 2023 05:55:28 +0100
Subject: [PATCH] feat(manager/gomod): add `goGetDirs` option (#20156)

Co-authored-by: Rhys Arkins <rhys@arkins.net>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
---
 docs/usage/configuration-options.md         |  11 ++
 docs/usage/golang.md                        |   2 +-
 lib/config/options/index.ts                 |   8 ++
 lib/modules/manager/gomod/artifacts.spec.ts | 110 +++++++++++++++++++-
 lib/modules/manager/gomod/artifacts.ts      |  22 +++-
 lib/modules/manager/types.ts                |   1 +
 6 files changed, 151 insertions(+), 3 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 775d6aad22..690946388f 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -1040,6 +1040,17 @@ Under the hood, it creates a MR-level approval rule where `approvals_required` i
 This option works only when `automerge=true`, `automergeType=pr` or `automergeType=branch`, and `platformAutomerge=true`.
 Also, approval rules overriding should not be [prevented in GitLab settings](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/settings.html#prevent-editing-approval-rules-in-merge-requests).
 
+## goGetDirs
+
+By default, Renovate will run `go get -d -t ./...` to update the `go.sum`.
+If you need to modify this path, for example in order to ignore directories, you can override the default `./...` value using this option:
+
+```json
+{
+  "goGetDirs": ["./some-project/", "./tools/..."]
+}
+```
+
 ## golang
 
 Configuration added here applies for all Go-related updates.
diff --git a/docs/usage/golang.md b/docs/usage/golang.md
index 705e294f20..fd560949a9 100644
--- a/docs/usage/golang.md
+++ b/docs/usage/golang.md
@@ -13,7 +13,7 @@ Renovate supports upgrading dependencies in `go.mod` files and their accompanyin
 1. Renovate extracts existing dependencies from `require` statements
 1. Renovate resolves the dependency's source repository and checks for SemVer tags if found. Otherwise commits and `v0.0.0-....` syntax will be used
 1. If Renovate finds an update, Renovate will update `go.mod` to the new value
-1. Renovate runs `go get` to update the `go.sum` files
+1. Renovate runs `go get` to update the `go.sum` files (you can configure which directory are included using the `goGetDirs` option)
 1. If the user has enabled the option `gomodUpdateImportPaths` in the [`postUpdateOptions`](https://docs.renovatebot.com/configuration-options/#postupdateoptions) array, then Renovate uses [mod](https://github.com/marwan-at-work/mod) to update import paths on major updates, which can update any Go source file
 1. If the user has any of the available `gomodTidy` options (e.g. `gomodTidy1.17`) in the [`postUpdateOptions`](https://docs.renovatebot.com/configuration-options/#postupdateoptions), then Renovate runs `go mod tidy` with the respective options (multiple options are allowed).
 1. `go mod vendor` is run if vendored modules are detected
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 4702833112..ae357e8d09 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -366,6 +366,14 @@ const options: RenovateOptions[] = [
     subType: 'string',
     default: [],
   },
+  {
+    name: 'goGetDirs',
+    description: 'Directory pattern to run `go get` on',
+    type: 'array',
+    subType: 'string',
+    default: ['./...'],
+    supportedManagers: ['gomod'],
+  },
   // Log options
   {
     name: 'logFile',
diff --git a/lib/modules/manager/gomod/artifacts.spec.ts b/lib/modules/manager/gomod/artifacts.spec.ts
index 667eee6bdd..22cfc117b7 100644
--- a/lib/modules/manager/gomod/artifacts.spec.ts
+++ b/lib/modules/manager/gomod/artifacts.spec.ts
@@ -15,7 +15,17 @@ jest.mock('../../../util/exec/env');
 jest.mock('../../../util/git');
 jest.mock('../../../util/host-rules');
 jest.mock('../../../util/http');
-jest.mock('../../../util/fs');
+jest.mock('../../../util/fs', () => {
+  // restore
+  return {
+    __esModules: true,
+    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+    ...(jest.createMockFromModule('../../../util/fs') as any),
+    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+    isValidLocalPath: (jest.requireActual('../../../util/fs') as any)
+      .isValidLocalPath,
+  };
+});
 jest.mock('../../datasource');
 
 process.env.BUILDPACK = 'true';
@@ -1939,4 +1949,102 @@ describe('modules/manager/gomod/artifacts', () => {
     ];
     expect(execSnapshots).toMatchObject(expectedResult);
   });
+
+  it('handles goGetDirs configuration correctly', async () => {
+    fs.readLocalFile.mockResolvedValueOnce('Current go.sum');
+    fs.readLocalFile.mockResolvedValueOnce(null); // vendor modules filename
+    const execSnapshots = mockExecAll();
+    git.getRepoStatus.mockResolvedValueOnce(
+      partial<StatusResult>({
+        modified: [],
+      })
+    );
+
+    expect(
+      await gomod.updateArtifacts({
+        packageFileName: 'go.mod',
+        updatedDeps: [],
+        newPackageFileContent: gomod1,
+        config: {
+          ...config,
+          goGetDirs: ['.', 'foo', '.bar/...', '&&', 'cat', '/etc/passwd'],
+        },
+      })
+    ).toBeNull();
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'go get -d -t . foo .bar/... cat',
+        options: {
+          cwd: '/tmp/github/some/repo',
+        },
+      },
+    ]);
+  });
+
+  it('returns updated go.sum when goGetDirs is specified', async () => {
+    fs.readLocalFile.mockResolvedValueOnce('Current go.sum');
+    fs.readLocalFile.mockResolvedValueOnce(null); // vendor modules filename
+    const execSnapshots = mockExecAll();
+    git.getRepoStatus.mockResolvedValueOnce(
+      partial<StatusResult>({
+        modified: ['go.sum'],
+      })
+    );
+    fs.readLocalFile.mockResolvedValueOnce('New go.sum');
+    fs.readLocalFile.mockResolvedValueOnce(gomod1);
+    expect(
+      await gomod.updateArtifacts({
+        packageFileName: 'go.mod',
+        updatedDeps: [],
+        newPackageFileContent: gomod1,
+        config: {
+          ...config,
+          goGetDirs: ['.'],
+        },
+      })
+    ).toEqual([
+      {
+        file: {
+          contents: 'New go.sum',
+          path: 'go.sum',
+          type: 'addition',
+        },
+      },
+    ]);
+    expect(execSnapshots).toMatchObject([
+      {
+        cmd: 'go get -d -t .',
+        options: {
+          cwd: '/tmp/github/some/repo',
+        },
+      },
+    ]);
+  });
+
+  it('errors when goGetDirs is specified with all invalid paths', async () => {
+    fs.readLocalFile.mockResolvedValueOnce('Current go.sum');
+    fs.readLocalFile.mockResolvedValueOnce(null); // vendor modules filename
+    const execSnapshots = mockExecAll();
+    git.getRepoStatus.mockResolvedValueOnce(
+      partial<StatusResult>({
+        modified: ['go.sum'],
+      })
+    );
+    fs.readLocalFile.mockResolvedValueOnce('New go.sum');
+    fs.readLocalFile.mockResolvedValueOnce(gomod1);
+    expect(
+      await gomod.updateArtifacts({
+        packageFileName: 'go.mod',
+        updatedDeps: [],
+        newPackageFileContent: gomod1,
+        config: {
+          ...config,
+          goGetDirs: ['&&', '||'],
+        },
+      })
+    ).toEqual([
+      { artifactError: { lockFile: 'go.sum', stderr: 'Invalid goGetDirs' } },
+    ]);
+    expect(execSnapshots).toMatchObject([]);
+  });
 });
diff --git a/lib/modules/manager/gomod/artifacts.ts b/lib/modules/manager/gomod/artifacts.ts
index 6ef9025014..0b6de5e39d 100644
--- a/lib/modules/manager/gomod/artifacts.ts
+++ b/lib/modules/manager/gomod/artifacts.ts
@@ -1,5 +1,6 @@
 import is from '@sindresorhus/is';
 import semver from 'semver';
+import { quote } from 'shlex';
 import upath from 'upath';
 import { GlobalConfig } from '../../../config/global';
 import type { PlatformId } from '../../../constants';
@@ -10,6 +11,7 @@ import { exec } from '../../../util/exec';
 import type { ExecOptions } from '../../../util/exec/types';
 import {
   ensureCacheDir,
+  isValidLocalPath,
   readLocalFile,
   writeLocalFile,
 } from '../../../util/fs';
@@ -296,7 +298,25 @@ export async function updateArtifacts({
 
     const execCommands: string[] = [];
 
-    let args = 'get -d -t ./...';
+    let goGetDirs: string | undefined;
+    if (config.goGetDirs) {
+      goGetDirs = config.goGetDirs
+        .filter((dir) => {
+          const isValid = isValidLocalPath(dir);
+          if (!isValid) {
+            logger.warn({ dir }, 'Invalid path in goGetDirs');
+          }
+          return isValid;
+        })
+        .map(quote)
+        .join(' ');
+
+      if (goGetDirs === '') {
+        throw new Error('Invalid goGetDirs');
+      }
+    }
+
+    let args = `get -d -t ${goGetDirs ?? './...'}`;
     logger.trace({ cmd, args }, 'go get command included');
     execCommands.push(`${cmd} ${args}`);
 
diff --git a/lib/modules/manager/types.ts b/lib/modules/manager/types.ts
index abd361b4b1..c81b729d1c 100644
--- a/lib/modules/manager/types.ts
+++ b/lib/modules/manager/types.ts
@@ -33,6 +33,7 @@ export interface UpdateArtifactsConfig {
   isLockFileMaintenance?: boolean;
   constraints?: Record<string, string>;
   composerIgnorePlatformReqs?: string[];
+  goGetDirs?: string[];
   currentValue?: string;
   postUpdateOptions?: string[];
   ignorePlugins?: boolean;
-- 
GitLab