diff --git a/lib/manager/bundler/artifacts.ts b/lib/manager/bundler/artifacts.ts index 0070ab298b49086e7289094b5c4ce3b271911b8b..c31689327c8e26061d13b8dd76bf3304d646f7b9 100644 --- a/lib/manager/bundler/artifacts.ts +++ b/lib/manager/bundler/artifacts.ts @@ -1,7 +1,9 @@ -import { outputFile, readFile } from 'fs-extra'; -import { join, dirname } from 'upath'; -import { exec } from '../../util/exec'; -import { getChildProcessEnv } from '../../util/exec/env'; +import { + getSiblingFileName, + readLocalFile, + writeLocalFile, +} from '../../util/fs'; +import { exec, ExecOptions } from '../../util/exec'; import { logger } from '../../logger'; import { getPkgReleases } from '../../datasource/docker'; import { @@ -16,122 +18,132 @@ import { BUNDLER_INVALID_CREDENTIALS, BUNDLER_UNKNOWN_ERROR, } from '../../constants/error-messages'; -import { BinarySource } from '../../util/exec/common'; -export async function updateArtifacts({ - packageFileName, - updatedDeps, - newPackageFileContent, - config, -}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> { +async function getRubyConstraint( + updateArtifact: UpdateArtifact +): Promise<string> { + const { packageFileName, config } = updateArtifact; + const { compatibility = {} } = config; + const { ruby } = compatibility; + + let rubyConstraint: string; + if (ruby) { + logger.debug('Using rubyConstraint from config'); + rubyConstraint = ruby; + } else { + const rubyVersionFile = getSiblingFileName( + packageFileName, + '.ruby-version' + ); + const rubyVersionFileContent = await platform.getFile(rubyVersionFile); + if (rubyVersionFileContent) { + logger.debug('Using ruby version specified in .ruby-version'); + rubyConstraint = rubyVersionFileContent + .replace(/^ruby-/, '') + .replace(/\n/g, '') + .trim(); + } + } + return rubyConstraint; +} + +async function getDockerTag(updateArtifact: UpdateArtifact): Promise<string> { + const constraint = await getRubyConstraint(updateArtifact); + if (!constraint) { + logger.debug('No ruby version constraint found, so using latest'); + return 'latest'; + } + if (!isValid(constraint)) { + logger.warn({ constraint }, 'Invalid ruby version constraint'); + return 'latest'; + } + logger.debug( + { constraint }, + 'Found ruby version constraint - checking for a compatible renovate/ruby image to use' + ); + const rubyReleases = await getPkgReleases({ + lookupName: 'renovate/ruby', + }); + // istanbul ignore else + if (rubyReleases && rubyReleases.releases) { + let versions = rubyReleases.releases.map(release => release.version); + versions = versions.filter( + version => isVersion(version) && matches(version, constraint) + ); + versions = versions.sort(sortVersions); + if (versions.length) { + const rubyVersion = versions.pop(); + logger.debug( + { constraint, rubyVersion }, + 'Found compatible ruby version' + ); + return rubyVersion; + } + } else { + logger.error('No renovate/ruby releases found'); + return 'latest'; + } + logger.warn( + { constraint }, + 'Failed to find a tag satisfying ruby constraint, using latest ruby image instead' + ); + return 'latest'; +} + +export async function updateArtifacts( + updateArtifact: UpdateArtifact +): Promise<UpdateArtifactsResult[] | null> { + const { + packageFileName, + updatedDeps, + newPackageFileContent, + config, + } = updateArtifact; + const { compatibility = {} } = config; + logger.debug(`bundler.updateArtifacts(${packageFileName})`); // istanbul ignore if if (global.repoCache.bundlerArtifactsError) { logger.info('Aborting Bundler artifacts due to previous failed attempt'); throw new Error(global.repoCache.bundlerArtifactsError); } - const lockFileName = packageFileName + '.lock'; + const lockFileName = `${packageFileName}.lock`; const existingLockFileContent = await platform.getFile(lockFileName); if (!existingLockFileContent) { logger.debug('No Gemfile.lock found'); return null; } - const cwd = join(config.localDir, dirname(packageFileName)); try { - const localPackageFileName = join(config.localDir, packageFileName); - await outputFile(localPackageFileName, newPackageFileContent); - const localLockFileName = join(config.localDir, lockFileName); - const env = getChildProcessEnv(); - let cmd; - if (config.binarySource === BinarySource.Docker) { - logger.info('Running bundler via docker'); - let tag = 'latest'; - let rubyConstraint: string; - if (config && config.compatibility && config.compatibility.ruby) { - logger.debug('Using rubyConstraint from config'); - rubyConstraint = config.compatibility.ruby; - } else { - const rubyVersionFile = join(dirname(packageFileName), '.ruby-version'); - logger.debug('Checking ' + rubyVersionFile); - const rubyVersionFileContent = await platform.getFile(rubyVersionFile); - if (rubyVersionFileContent) { - logger.debug('Using ruby version specified in .ruby-version'); - rubyConstraint = rubyVersionFileContent - .replace(/^ruby-/, '') - .replace(/\n/g, '') - .trim(); - } - } - if (rubyConstraint && isValid(rubyConstraint)) { - logger.debug({ rubyConstraint }, 'Found ruby compatibility'); - const rubyReleases = await getPkgReleases({ - lookupName: 'renovate/ruby', - }); - if (rubyReleases && rubyReleases.releases) { - let versions = rubyReleases.releases.map(release => release.version); - versions = versions.filter(version => isVersion(version)); - versions = versions.filter(version => - matches(version, rubyConstraint) - ); - versions = versions.sort(sortVersions); - if (versions.length) { - tag = versions.pop(); - } - } - if (tag === 'latest') { - logger.warn( - { rubyConstraint }, - 'Failed to find a tag satisfying ruby constraint, using latest ruby image instead' - ); - } - } - const bundlerConstraint = - config && config.compatibility && config.compatibility.bundler - ? config.compatibility.bundler - : undefined; - let bundlerVersion = ''; - if (bundlerConstraint && isVersion(bundlerConstraint)) { - bundlerVersion = ' -v ' + bundlerConstraint; - } - cmd = `docker run --rm `; - if (config.dockerUser) { - cmd += `--user=${config.dockerUser} `; - } - const volumes = [config.localDir]; - cmd += volumes.map(v => `-v "${v}":"${v}" `).join(''); - cmd += `-w "${cwd}" `; - cmd += `renovate/ruby:${tag} bash -l -c "ruby --version && `; - cmd += 'gem install bundler' + bundlerVersion + ' --no-document'; - cmd += ' && bundle'; - } else if ( - config.binarySource === BinarySource.Auto || - config.binarySource === BinarySource.Global - ) { - logger.info('Running bundler via global bundler'); - cmd = 'bundle'; - } else { - logger.warn({ config }, 'Unsupported binarySource'); - cmd = 'bundle'; - } - cmd += ` lock --update ${updatedDeps.join(' ')}`; - if (cmd.includes('bash -l -c "')) { - cmd += '"'; - } - logger.debug({ cmd }, 'bundler command'); - await exec(cmd, { - cwd, - env, - }); + await writeLocalFile(packageFileName, newPackageFileContent); + + const cmd = `bundle lock --update ${updatedDeps.join(' ')}`; + + const { bundler } = compatibility; + const bundlerVersion = bundler && isValid(bundler) ? ` -v ${bundler}` : ''; + const preCommands = [ + 'ruby --version', + `gem install bundler${bundlerVersion} --no-document`, + ]; + + const execOptions: ExecOptions = { + docker: { + image: 'renovate/ruby', + tag: await getDockerTag(updateArtifact), + preCommands, + }, + }; + await exec(cmd, execOptions); const status = await platform.getRepoStatus(); if (!status.modified.includes(lockFileName)) { return null; } logger.debug('Returning updated Gemfile.lock'); + const lockFileContent = await readLocalFile(lockFileName); return [ { file: { name: lockFileName, - contents: await readFile(localLockFileName, 'utf8'), + contents: lockFileContent, }, }, ]; diff --git a/test/manager/bundler/__snapshots__/artifacts.spec.ts.snap b/test/manager/bundler/__snapshots__/artifacts.spec.ts.snap index 9ba7eff927b0b1189f669e03e1c03a1ffa6d18a6..4b19918f81f23c0a3479aa59044eb19eda63c0bd 100644 --- a/test/manager/bundler/__snapshots__/artifacts.spec.ts.snap +++ b/test/manager/bundler/__snapshots__/artifacts.spec.ts.snap @@ -14,7 +14,13 @@ Array [ exports[`bundler.updateArtifacts() Docker .ruby-version 2`] = ` Array [ Object { - "cmd": "docker run --rm -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/ruby:1.2.0 bash -l -c \\"ruby --version && gem install bundler --no-document && bundle lock --update \\"", + "cmd": "docker pull renovate/ruby:1.2.0", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/ruby:1.2.0 bash -l -c \\"ruby --version && gem install bundler --no-document && bundle lock --update foo bar\\"", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", @@ -46,7 +52,51 @@ Array [ exports[`bundler.updateArtifacts() Docker compatibility options 2`] = ` Array [ Object { - "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/ruby:latest bash -l -c \\"ruby --version && gem install bundler -v 3.2.1 --no-document && bundle lock --update \\"", + "cmd": "docker pull renovate/ruby:latest", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/ruby:latest bash -l -c \\"ruby --version && gem install bundler -v 3.2.1 --no-document && bundle lock --update foo bar\\"", + "options": Object { + "cwd": "/tmp/github/some/repo", + "encoding": "utf-8", + "env": Object { + "HOME": "/home/user", + "HTTPS_PROXY": "https://example.com", + "HTTP_PROXY": "http://example.com", + "LANG": "en_US.UTF-8", + "LC_ALL": "en_US", + "NO_PROXY": "localhost", + "PATH": "/tmp/path", + }, + }, + }, +] +`; + +exports[`bundler.updateArtifacts() Docker invalid compatibility options 1`] = ` +Array [ + Object { + "file": Object { + "contents": "Updated Gemfile.lock", + "name": "Gemfile.lock", + }, + }, +] +`; + +exports[`bundler.updateArtifacts() Docker invalid compatibility options 2`] = ` +Array [ + Object { + "cmd": "docker pull renovate/ruby:latest", + "options": Object { + "encoding": "utf-8", + }, + }, + Object { + "cmd": "docker run --rm --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -w \\"/tmp/github/some/repo\\" renovate/ruby:latest bash -l -c \\"ruby --version && gem install bundler --no-document && bundle lock --update foo bar\\"", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", @@ -69,7 +119,7 @@ exports[`bundler.updateArtifacts() returns null if Gemfile.lock was not changed exports[`bundler.updateArtifacts() returns null if Gemfile.lock was not changed 2`] = ` Array [ Object { - "cmd": "bundle lock --update ", + "cmd": "bundle lock --update foo bar", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", @@ -101,7 +151,7 @@ Array [ exports[`bundler.updateArtifacts() works explicit global binarySource 2`] = ` Array [ Object { - "cmd": "bundle lock --update ", + "cmd": "bundle lock --update foo bar", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", @@ -133,7 +183,7 @@ Array [ exports[`bundler.updateArtifacts() works for default binarySource 2`] = ` Array [ Object { - "cmd": "bundle lock --update ", + "cmd": "bundle lock --update foo bar", "options": Object { "cwd": "/tmp/github/some/repo", "encoding": "utf-8", diff --git a/test/manager/bundler/artifacts.spec.ts b/test/manager/bundler/artifacts.spec.ts index f6b537d835c279ca2413344f81ffa6ea6526f754..26b15659b4bfb800790c007c1b68a1ecfc1d35bf 100644 --- a/test/manager/bundler/artifacts.spec.ts +++ b/test/manager/bundler/artifacts.spec.ts @@ -1,3 +1,4 @@ +import { join } from 'upath'; import _fs from 'fs-extra'; import { exec as _exec } from 'child_process'; import Git from 'simple-git/promise'; @@ -8,6 +9,8 @@ import { mocked } from '../../util'; import { envMock, mockExecAll } from '../../execUtil'; import * as _env from '../../../lib/util/exec/env'; import { BinarySource } from '../../../lib/util/exec/common'; +import { setUtilConfig } from '../../../lib/util'; +import { resetPrefetchedImages } from '../../../lib/util/exec/docker'; const fs: jest.Mocked<typeof _fs> = _fs as any; const exec: jest.Mock<typeof _exec> = _exec as any; @@ -29,16 +32,20 @@ describe('bundler.updateArtifacts()', () => { jest.resetModules(); config = { - localDir: '/tmp/github/some/repo', + // `join` fixes Windows CI + localDir: join('/tmp/github/some/repo'), + dockerUser: 'foobar', }; env.getChildProcessEnv.mockReturnValue(envMock.basic); + resetPrefetchedImages(); + setUtilConfig(config); }); it('returns null by default', async () => { expect( await updateArtifacts({ packageFileName: '', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: '', config, }) @@ -55,7 +62,7 @@ describe('bundler.updateArtifacts()', () => { expect( await updateArtifacts({ packageFileName: 'Gemfile', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: 'Updated Gemfile content', config, }) @@ -73,7 +80,7 @@ describe('bundler.updateArtifacts()', () => { expect( await updateArtifacts({ packageFileName: 'Gemfile', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: 'Updated Gemfile content', config, }) @@ -91,7 +98,7 @@ describe('bundler.updateArtifacts()', () => { expect( await updateArtifacts({ packageFileName: 'Gemfile', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: 'Updated Gemfile content', config: { ...config, @@ -102,6 +109,9 @@ describe('bundler.updateArtifacts()', () => { expect(execSnapshots).toMatchSnapshot(); }); describe('Docker', () => { + beforeEach(() => { + setUtilConfig({ ...config, binarySource: BinarySource.Docker }); + }); it('.ruby-version', async () => { platform.getFile.mockResolvedValueOnce('Current Gemfile.lock'); fs.outputFile.mockResolvedValueOnce(null as never); @@ -121,7 +131,7 @@ describe('bundler.updateArtifacts()', () => { expect( await updateArtifacts({ packageFileName: 'Gemfile', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: 'Updated Gemfile content', config: { ...config, @@ -149,7 +159,7 @@ describe('bundler.updateArtifacts()', () => { expect( await updateArtifacts({ packageFileName: 'Gemfile', - updatedDeps: [], + updatedDeps: ['foo', 'bar'], newPackageFileContent: 'Updated Gemfile content', config: { ...config, @@ -164,5 +174,38 @@ describe('bundler.updateArtifacts()', () => { ).toMatchSnapshot(); expect(execSnapshots).toMatchSnapshot(); }); + it('invalid compatibility options', async () => { + platform.getFile.mockResolvedValueOnce('Current Gemfile.lock'); + fs.outputFile.mockResolvedValueOnce(null as never); + datasource.getPkgReleases.mockResolvedValueOnce({ + releases: [ + { version: '1.0.0' }, + { version: '1.2.0' }, + { version: '1.3.0' }, + ], + }); + const execSnapshots = mockExecAll(exec); + platform.getRepoStatus.mockResolvedValueOnce({ + modified: ['Gemfile.lock'], + } as Git.StatusResult); + fs.readFile.mockResolvedValueOnce('Updated Gemfile.lock' as any); + expect( + await updateArtifacts({ + packageFileName: 'Gemfile', + updatedDeps: ['foo', 'bar'], + newPackageFileContent: 'Updated Gemfile content', + config: { + ...config, + binarySource: BinarySource.Docker, + dockerUser: 'foobar', + compatibility: { + ruby: 'foo', + bundler: 'bar', + }, + }, + }) + ).toMatchSnapshot(); + expect(execSnapshots).toMatchSnapshot(); + }); }); });