From 478e84a547888bbacf3d08d87955e01069470770 Mon Sep 17 00:00:00 2001 From: Chance Zibolski Date: Fri, 17 May 2024 16:42:16 -0700 Subject: [PATCH] feat(gomod): Support workspace vendoring If go.work is present and a vendor/ is present, then the module is using go workspaces with vendoring, recently introduced with Go 1.22 (see https://github.com/golang/go/issues/60056 for details). When using Go workspaces, you must use `go work vendor` instead of `go mod vendor`. --- lib/modules/manager/gomod/artifacts.spec.ts | 117 +++++++++++++++++++- lib/modules/manager/gomod/artifacts.ts | 47 ++++++-- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/lib/modules/manager/gomod/artifacts.spec.ts b/lib/modules/manager/gomod/artifacts.spec.ts index 90b18aad543ae5..b0986058f70650 100644 --- a/lib/modules/manager/gomod/artifacts.spec.ts +++ b/lib/modules/manager/gomod/artifacts.spec.ts @@ -1,7 +1,7 @@ import { codeBlock } from 'common-tags'; import { mockDeep } from 'jest-mock-extended'; import { join } from 'upath'; -import { envMock, mockExecAll } from '../../../../test/exec-util'; +import { envMock, mockExecAll, mockExecSequence } from '../../../../test/exec-util'; import { env, fs, git, mocked, partial } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { RepoGlobalConfig } from '../../../config/types'; @@ -252,6 +252,10 @@ describe('modules/manager/gomod/artifacts', () => { ]); expect(execSnapshots).toMatchObject([ + { + cmd: 'go env GOWORK', + options: { cwd: '/tmp/github/some/repo' }, + }, { cmd: 'go get -d -t ./...', options: { cwd: '/tmp/github/some/repo' }, @@ -271,6 +275,117 @@ describe('modules/manager/gomod/artifacts', () => { ]); }); + it('supports vendor directory update with go.work', async () => { + const foo = join('vendor/github.com/foo/foo/go.mod'); + const bar = join('vendor/github.com/bar/bar/go.mod'); + const baz = join('vendor/github.com/baz/baz/go.mod'); + + fs.readLocalFile.mockResolvedValueOnce('Current go.sum'); + fs.readLocalFile.mockResolvedValueOnce('modules.txt content'); // vendor modules filename + fs.readLocalFile.mockResolvedValueOnce('Current go.work'); // go.work + const execSnapshots = mockExecSequence([ + // Set the output returned by go env GOWORK + {stdout: '/tmp/github/some/repo/go.work', stderr: ''}, + // Remaining output does not matter + {stdout: '', stderr: ''}, + {stdout: '', stderr: ''}, + {stdout: '', stderr: ''}, + {stdout: '', stderr: ''}, + {stdout: '', stderr: ''}, + ]); + git.getRepoStatus.mockResolvedValueOnce( + partial({ + modified: ['go.sum', 'go.work.sum', foo], + not_added: [bar], + deleted: [baz], + }), + ); + fs.readLocalFile.mockResolvedValueOnce('New go.sum'); + fs.readLocalFile.mockResolvedValueOnce('New go.work.sum'); + fs.readLocalFile.mockResolvedValueOnce('Foo go.sum'); + fs.readLocalFile.mockResolvedValueOnce('Bar go.sum'); + fs.readLocalFile.mockResolvedValueOnce('New go.mod'); + const res = await gomod.updateArtifacts({ + packageFileName: 'go.mod', + updatedDeps: [], + newPackageFileContent: gomod1, + config: { + ...config, + postUpdateOptions: ['gomodTidy'], + }, + }); + expect(res).toEqual([ + { + file: { + contents: 'New go.sum', + path: 'go.sum', + type: 'addition', + }, + }, + { + file: { + contents: 'New go.work.sum', + path: 'go.work.sum', + type: 'addition', + }, + }, + { + file: { + contents: 'Foo go.sum', + path: 'vendor/github.com/foo/foo/go.mod', + type: 'addition', + }, + }, + { + file: { + contents: 'Bar go.sum', + path: 'vendor/github.com/bar/bar/go.mod', + type: 'addition', + }, + }, + { + file: { + path: 'vendor/github.com/baz/baz/go.mod', + type: 'deletion', + }, + }, + { + file: { + contents: 'New go.mod', + path: 'go.mod', + type: 'addition', + }, + }, + ]); + + expect(execSnapshots).toMatchObject([ + { + cmd: 'go env GOWORK', + options: { cwd: '/tmp/github/some/repo' }, + }, + { + cmd: 'go get -d -t ./...', + options: { cwd: '/tmp/github/some/repo' }, + }, + { + cmd: 'go mod tidy', + options: { cwd: '/tmp/github/some/repo' }, + }, + { + cmd: 'go work vendor', + options: { cwd: '/tmp/github/some/repo' }, + }, + { + cmd: 'go work sync', + options: { cwd: '/tmp/github/some/repo' }, + }, + { + cmd: 'go mod tidy', + options: { cwd: '/tmp/github/some/repo' }, + }, + ]); + }); + it('supports docker mode without credentials', async () => { GlobalConfig.set({ ...adminConfig, binarySource: 'docker' }); fs.readLocalFile.mockResolvedValueOnce('Current go.sum'); diff --git a/lib/modules/manager/gomod/artifacts.ts b/lib/modules/manager/gomod/artifacts.ts index 1ab191c5992a9a..98333ef124da24 100644 --- a/lib/modules/manager/gomod/artifacts.ts +++ b/lib/modules/manager/gomod/artifacts.ts @@ -126,7 +126,9 @@ export async function updateArtifacts({ return null; } - const vendorDir = upath.join(upath.dirname(goModFileName), 'vendor/'); + const goModDir = upath.dirname(goModFileName); + + const vendorDir = upath.join(goModDir, 'vendor/'); const vendorModulesFileName = upath.join(vendorDir, 'modules.txt'); const useVendor = (await readLocalFile(vendorModulesFileName)) !== null; @@ -235,7 +237,6 @@ export async function updateArtifacts({ throw new Error('Invalid goGetDirs'); } } - let args = `get -d -t ${goGetDirs ?? './...'}`; logger.trace({ cmd, args }, 'go get command included'); execCommands.push(`${cmd} ${args}`); @@ -275,18 +276,38 @@ export async function updateArtifacts({ config.postUpdateOptions?.includes('gomodTidy1.17') === true || config.postUpdateOptions?.includes('gomodTidyE') === true || (config.updateType === 'major' && isImportPathUpdateRequired)); + if (isGoModTidyRequired) { args = 'mod tidy' + tidyOpts; logger.debug('go mod tidy command included'); execCommands.push(`${cmd} ${args}`); } + const goWorkSumFileName = upath.join(goModDir, 'go.work.sum'); if (useVendor) { - args = 'mod vendor'; - logger.debug('go mod tidy command included'); - execCommands.push(`${cmd} ${args}`); - } + // If go env GOWORK returns a non-empty path, check that it exists and if + // it does, then use go workspace vendoring. + const goWorkEnv = await exec(`${cmd} env GOWORK`, execOptions); + const goWorkFile = goWorkEnv?.stdout?.trim() || ''; + const useGoWork = goWorkFile.length && ((await readLocalFile(goWorkFile)) !== null); + if (useGoWork) { + logger.debug('No go.work found'); + } + if (useGoWork) { + args = 'work vendor'; + logger.debug('using go work vendor'); + execCommands.push(`${cmd} ${args}`); + + args = 'work sync'; + logger.debug('using go work sync'); + execCommands.push(`${cmd} ${args}`); + } else { + args = 'mod vendor'; + logger.debug('using go mod vendor'); + execCommands.push(`${cmd} ${args}`); + } + } // We tidy one more time as a solution for #6795 if (isGoModTidyRequired) { args = 'mod tidy' + tidyOpts; @@ -299,7 +320,8 @@ export async function updateArtifacts({ const status = await getRepoStatus(); if ( !status.modified.includes(sumFileName) && - !status.modified.includes(goModFileName) + !status.modified.includes(goModFileName) && + !status.modified.includes(goWorkSumFileName) ) { return null; } @@ -316,6 +338,17 @@ export async function updateArtifacts({ }); } + if (status.modified.includes(goWorkSumFileName)) { + logger.debug('Returning updated go.work.sum'); + res.push({ + file: { + type: 'addition', + path: goWorkSumFileName, + contents: await readLocalFile(goWorkSumFileName), + }, + }); + } + // Include all the .go file import changes if (isImportPathUpdateRequired) { logger.debug('Returning updated go source files for import path changes');