Skip to content
Snippets Groups Projects
Unverified Commit 8c62f436 authored by Sergio Zharinov's avatar Sergio Zharinov Committed by GitHub
Browse files

feat(bundler): Use centralized docker execution (#5058)


Co-authored-by: default avatarRhys Arkins <rhys@arkins.net>
parent 6bf18be2
No related branches found
No related tags found
No related merge requests found
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,43 +18,23 @@ 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> {
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 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';
async function getRubyConstraint(
updateArtifact: UpdateArtifact
): Promise<string> {
const { packageFileName, config } = updateArtifact;
const { compatibility = {} } = config;
const { ruby } = compatibility;
let rubyConstraint: string;
if (config && config.compatibility && config.compatibility.ruby) {
if (ruby) {
logger.debug('Using rubyConstraint from config');
rubyConstraint = config.compatibility.ruby;
rubyConstraint = ruby;
} else {
const rubyVersionFile = join(dirname(packageFileName), '.ruby-version');
logger.debug('Checking ' + rubyVersionFile);
const rubyVersionFile = getSiblingFileName(
packageFileName,
'.ruby-version'
);
const rubyVersionFileContent = await platform.getFile(rubyVersionFile);
if (rubyVersionFileContent) {
logger.debug('Using ruby version specified in .ruby-version');
......@@ -62,76 +44,106 @@ export async function updateArtifacts({
.trim();
}
}
if (rubyConstraint && isValid(rubyConstraint)) {
logger.debug({ rubyConstraint }, 'Found ruby compatibility');
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));
versions = versions.filter(version =>
matches(version, rubyConstraint)
versions = versions.filter(
version => isVersion(version) && matches(version, constraint)
);
versions = versions.sort(sortVersions);
if (versions.length) {
tag = versions.pop();
const rubyVersion = versions.pop();
logger.debug(
{ constraint, rubyVersion },
'Found compatible ruby version'
);
return rubyVersion;
}
} else {
logger.error('No renovate/ruby releases found');
return 'latest';
}
if (tag === 'latest') {
logger.warn(
{ rubyConstraint },
{ 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 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 += '"';
const lockFileName = `${packageFileName}.lock`;
const existingLockFileContent = await platform.getFile(lockFileName);
if (!existingLockFileContent) {
logger.debug('No Gemfile.lock found');
return null;
}
logger.debug({ cmd }, 'bundler command');
await exec(cmd, {
cwd,
env,
});
try {
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,
},
},
];
......
......@@ -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",
......
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();
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment