Skip to content

Commit

Permalink
feat: evaluate buildpack constraints in exec (#12609)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarkins committed Nov 12, 2021
1 parent 6732fce commit 04620d7
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 98 deletions.
11 changes: 6 additions & 5 deletions lib/manager/composer/artifacts.ts
Expand Up @@ -8,6 +8,7 @@ import {
import * as datasourcePackagist from '../../datasource/packagist';
import { logger } from '../../logger';
import { ExecOptions, exec } from '../../util/exec';
import type { ToolConstraint } from '../../util/exec/types';
import {
ensureCacheDir,
ensureLocalDir,
Expand All @@ -25,7 +26,6 @@ import {
composerVersioningId,
extractContraints,
getComposerArguments,
getComposerConstraint,
getPhpConstraint,
} from './utils';

Expand Down Expand Up @@ -102,18 +102,19 @@ export async function updateArtifacts({
...config.constraints,
};

const preCommands: string[] = [
`install-tool composer ${await getComposerConstraint(constraints)}`,
];
const composerToolConstraint: ToolConstraint = {
toolName: 'composer',
constraint: constraints.composer,
};

const execOptions: ExecOptions = {
cwdFile: packageFileName,
extraEnv: {
COMPOSER_CACHE_DIR: await ensureCacheDir('composer'),
COMPOSER_AUTH: getAuthJson(),
},
toolConstraints: [composerToolConstraint],
docker: {
preCommands,
image: 'php',
tagConstraint: getPhpConstraint(constraints),
tagScheme: composerVersioningId,
Expand Down
53 changes: 1 addition & 52 deletions lib/manager/composer/utils.spec.ts
@@ -1,58 +1,7 @@
import { mocked } from '../../../test/util';
import { setGlobalConfig } from '../../config/global';
import * as _datasource from '../../datasource';
import {
extractContraints,
getComposerArguments,
getComposerConstraint,
} from './utils';

jest.mock('../../../lib/datasource');

const datasource = mocked(_datasource);
import { extractContraints, getComposerArguments } from './utils';

describe('manager/composer/utils', () => {
describe('getComposerConstraint', () => {
beforeEach(() => {
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.1.0' },
{ version: '1.3.0' },
{ version: '2.0.14' },
{ version: '2.1.0' },
],
});
});
it('returns from config', async () => {
expect(await getComposerConstraint({ composer: '1.1.0' })).toBe('1.1.0');
});

it('returns from latest', async () => {
expect(await getComposerConstraint({})).toBe('2.1.0');
});

it('throws no releases', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [],
});
await expect(getComposerConstraint({})).rejects.toThrow(
'No composer releases found.'
);
});

it('throws no compatible releases', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [{ version: '1.2.3' }],
});
await expect(
getComposerConstraint({ composer: '^3.1.0' })
).rejects.toThrow('No compatible composer releases found.');
});
});

describe('extractContraints', () => {
it('returns from require', () => {
expect(
Expand Down
40 changes: 0 additions & 40 deletions lib/manager/composer/utils.ts
@@ -1,6 +1,5 @@
import { quote } from 'shlex';
import { getGlobalConfig } from '../../config/global';
import { getPkgReleases } from '../../datasource';
import { logger } from '../../logger';
import { api, id as composerVersioningId } from '../../versioning/composer';
import type { UpdateArtifactsConfig } from '../types';
Expand Down Expand Up @@ -29,45 +28,6 @@ export function getComposerArguments(config: UpdateArtifactsConfig): string {
return args;
}

export async function getComposerConstraint(
constraints: Record<string, string>
): Promise<string> {
const { composer } = constraints;

if (api.isSingleVersion(composer)) {
logger.debug(
{ version: composer },
'Using composer constraint from config'
);
return composer;
}

const release = await getPkgReleases({
depName: 'composer/composer',
datasource: 'github-releases',
versioning: composerVersioningId,
});

if (!release?.releases?.length) {
throw new Error('No composer releases found.');
}
let versions = release.releases.map((r) => r.version);

if (composer) {
versions = versions.filter(
(v) => api.isValid(v) && api.matches(v, composer)
);
}

if (!versions.length) {
throw new Error('No compatible composer releases found.');
}

const version = versions.pop();
logger.debug({ range: composer, version }, 'Using composer constraint');
return version;
}

export function getPhpConstraint(constraints: Record<string, string>): string {
const { php } = constraints;

Expand Down
99 changes: 99 additions & 0 deletions lib/util/exec/buildpack.spec.ts
@@ -0,0 +1,99 @@
import { mocked } from '../../../test/util';
import * as _datasource from '../../datasource';
import { generateInstallCommands, resolveConstraint } from './buildpack';
import type { ToolConstraint } from './types';

jest.mock('../../../lib/datasource');

const datasource = mocked(_datasource);

describe('util/exec/buildpack', () => {
describe('resolveConstraint()', () => {
beforeEach(() => {
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.1.0' },
{ version: '1.3.0' },
{ version: '2.0.14' },
{ version: '2.1.0' },
],
});
});
it('returns from config', async () => {
expect(
await resolveConstraint({ toolName: 'composer', constraint: '1.1.0' })
).toBe('1.1.0');
});

it('returns from latest', async () => {
expect(await resolveConstraint({ toolName: 'composer' })).toBe('2.1.0');
});

it('throws for unknown tools', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [],
});
await expect(resolveConstraint({ toolName: 'whoops' })).rejects.toThrow(
'Invalid tool to install: whoops'
);
});

it('throws no releases', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [],
});
await expect(resolveConstraint({ toolName: 'composer' })).rejects.toThrow(
'No tool releases found.'
);
});

it('falls back to latest version if no compatible release', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [{ version: '1.2.3' }],
});
expect(
await resolveConstraint({ toolName: 'composer', constraint: '^3.1.0' })
).toBe('1.2.3');
});

it('falls back to latest version if invalid constraint', async () => {
datasource.getPkgReleases.mockReset();
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [{ version: '1.2.3' }],
});
expect(
await resolveConstraint({ toolName: 'composer', constraint: 'whoops' })
).toBe('1.2.3');
});
});
describe('generateInstallCommands()', () => {
beforeEach(() => {
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.1.0' },
{ version: '1.3.0' },
{ version: '2.0.14' },
{ version: '2.1.0' },
],
});
});
it('returns install commands', async () => {
const toolConstraints: ToolConstraint[] = [
{
toolName: 'composer',
},
];
expect(await generateInstallCommands(toolConstraints))
.toMatchInlineSnapshot(`
Array [
"install-tool composer 2.1.0",
]
`);
});
});
});
75 changes: 75 additions & 0 deletions lib/util/exec/buildpack.ts
@@ -0,0 +1,75 @@
import { quote } from 'shlex';
import { getPkgReleases } from '../../datasource';
import { logger } from '../../logger';
import * as allVersioning from '../../versioning';
import { id as composerVersioningId } from '../../versioning/composer';
import type { ToolConfig, ToolConstraint } from './types';

const allToolConfig: Record<string, ToolConfig> = {
composer: {
datasource: 'github-releases',
depName: 'composer/composer',
versioning: composerVersioningId,
},
};

export async function resolveConstraint(
toolConstraint: ToolConstraint
): Promise<string> {
const { toolName } = toolConstraint;
const toolConfig = allToolConfig[toolName];
if (!toolConfig) {
throw new Error(`Invalid tool to install: ${toolName}`);
}

const versioning = allVersioning.get(toolConfig.versioning);
let constraint = toolConstraint.constraint;
if (constraint) {
if (versioning.isValid(constraint)) {
if (versioning.isSingleVersion(constraint)) {
return constraint;
}
} else {
logger.warn({ toolName, constraint }, 'Invalid tool constraint');
constraint = undefined;
}
}

const pkgReleases = await getPkgReleases(toolConfig);
if (!pkgReleases?.releases?.length) {
throw new Error('No tool releases found.');
}

const allVersions = pkgReleases.releases.map((r) => r.version);
const matchingVersions = allVersions.filter(
(v) => !constraint || versioning.matches(v, constraint)
);

if (matchingVersions.length) {
const resolvedVersion = matchingVersions.pop();
logger.debug({ toolName, constraint, resolvedVersion }, 'Resolved version');
return resolvedVersion;
}
const latestVersion = allVersions.filter((v) => versioning.isStable(v)).pop();
logger.warn(
{ toolName, constraint, latestVersion },
'No matching tool versions found for constraint - using latest version'
);
return latestVersion;
}

export async function generateInstallCommands(
toolConstraints: ToolConstraint[]
): Promise<string[]> {
const installCommands = [];
if (toolConstraints?.length) {
for (const toolConstraint of toolConstraints) {
const toolVersion = await resolveConstraint(toolConstraint);
const installCommand = `install-tool ${toolConstraint.toolName} ${quote(
toolVersion
)}`;
installCommands.push(installCommand);
}
}
return installCommands;
}
9 changes: 8 additions & 1 deletion lib/util/exec/index.ts
Expand Up @@ -3,6 +3,7 @@ import { dirname, join } from 'upath';
import { getGlobalConfig } from '../../config/global';
import { TEMPORARY_ERROR } from '../../constants/error-messages';
import { logger } from '../../logger';
import { generateInstallCommands } from './buildpack';
import {
DockerOptions,
ExecResult,
Expand All @@ -12,13 +13,15 @@ import {
} from './common';
import { generateDockerCommand, removeDockerContainer } from './docker';
import { getChildProcessEnv } from './env';
import type { ToolConstraint } from './types';

type ExtraEnv<T = unknown> = Record<string, T>;

export interface ExecOptions extends ChildProcessExecOptions {
cwdFile?: string;
extraEnv?: Opt<ExtraEnv>;
docker?: Opt<DockerOptions>;
toolConstraints?: Opt<ToolConstraint[]>;
}

function getChildEnv({
Expand Down Expand Up @@ -69,6 +72,7 @@ function getRawExecOptions(opts: ExecOptions): RawExecOptions {
delete execOptions.extraEnv;
delete execOptions.docker;
delete execOptions.cwdFile;
delete execOptions.toolConstraints;

const childEnv = getChildEnv(opts);
const cwd = getCwd(opts);
Expand Down Expand Up @@ -113,7 +117,10 @@ async function prepareRawExec(
const envVars = dockerEnvVars(extraEnv, childEnv);
const cwd = getCwd(opts);
const dockerOptions: DockerOptions = { ...docker, cwd, envVars };

dockerOptions.preCommands = [
...(await generateInstallCommands(opts.toolConstraints)),
...(dockerOptions.preCommands || []),
];
const dockerCommand = await generateDockerCommand(
rawCommands,
dockerOptions
Expand Down
10 changes: 10 additions & 0 deletions lib/util/exec/types.ts
@@ -0,0 +1,10 @@
export interface ToolConstraint {
toolName: string;
constraint?: string;
}

export interface ToolConfig {
datasource: string;
depName: string;
versioning: string;
}

0 comments on commit 04620d7

Please sign in to comment.