Skip to content
Snippets Groups Projects
Unverified Commit 04620d71 authored by Rhys Arkins's avatar Rhys Arkins Committed by GitHub
Browse files

feat: evaluate buildpack constraints in exec (#12609)

parent 6732fce9
No related branches found
No related tags found
No related merge requests found
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
import * as datasourcePackagist from '../../datasource/packagist'; import * as datasourcePackagist from '../../datasource/packagist';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { ExecOptions, exec } from '../../util/exec'; import { ExecOptions, exec } from '../../util/exec';
import type { ToolConstraint } from '../../util/exec/types';
import { import {
ensureCacheDir, ensureCacheDir,
ensureLocalDir, ensureLocalDir,
...@@ -25,7 +26,6 @@ import { ...@@ -25,7 +26,6 @@ import {
composerVersioningId, composerVersioningId,
extractContraints, extractContraints,
getComposerArguments, getComposerArguments,
getComposerConstraint,
getPhpConstraint, getPhpConstraint,
} from './utils'; } from './utils';
...@@ -102,9 +102,10 @@ export async function updateArtifacts({ ...@@ -102,9 +102,10 @@ export async function updateArtifacts({
...config.constraints, ...config.constraints,
}; };
const preCommands: string[] = [ const composerToolConstraint: ToolConstraint = {
`install-tool composer ${await getComposerConstraint(constraints)}`, toolName: 'composer',
]; constraint: constraints.composer,
};
const execOptions: ExecOptions = { const execOptions: ExecOptions = {
cwdFile: packageFileName, cwdFile: packageFileName,
...@@ -112,8 +113,8 @@ export async function updateArtifacts({ ...@@ -112,8 +113,8 @@ export async function updateArtifacts({
COMPOSER_CACHE_DIR: await ensureCacheDir('composer'), COMPOSER_CACHE_DIR: await ensureCacheDir('composer'),
COMPOSER_AUTH: getAuthJson(), COMPOSER_AUTH: getAuthJson(),
}, },
toolConstraints: [composerToolConstraint],
docker: { docker: {
preCommands,
image: 'php', image: 'php',
tagConstraint: getPhpConstraint(constraints), tagConstraint: getPhpConstraint(constraints),
tagScheme: composerVersioningId, tagScheme: composerVersioningId,
......
import { mocked } from '../../../test/util';
import { setGlobalConfig } from '../../config/global'; import { setGlobalConfig } from '../../config/global';
import * as _datasource from '../../datasource'; import { extractContraints, getComposerArguments } from './utils';
import {
extractContraints,
getComposerArguments,
getComposerConstraint,
} from './utils';
jest.mock('../../../lib/datasource');
const datasource = mocked(_datasource);
describe('manager/composer/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', () => { describe('extractContraints', () => {
it('returns from require', () => { it('returns from require', () => {
expect( expect(
......
import { quote } from 'shlex'; import { quote } from 'shlex';
import { getGlobalConfig } from '../../config/global'; import { getGlobalConfig } from '../../config/global';
import { getPkgReleases } from '../../datasource';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { api, id as composerVersioningId } from '../../versioning/composer'; import { api, id as composerVersioningId } from '../../versioning/composer';
import type { UpdateArtifactsConfig } from '../types'; import type { UpdateArtifactsConfig } from '../types';
...@@ -29,45 +28,6 @@ export function getComposerArguments(config: UpdateArtifactsConfig): string { ...@@ -29,45 +28,6 @@ export function getComposerArguments(config: UpdateArtifactsConfig): string {
return args; 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 { export function getPhpConstraint(constraints: Record<string, string>): string {
const { php } = constraints; const { php } = constraints;
......
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",
]
`);
});
});
});
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;
}
...@@ -3,6 +3,7 @@ import { dirname, join } from 'upath'; ...@@ -3,6 +3,7 @@ import { dirname, join } from 'upath';
import { getGlobalConfig } from '../../config/global'; import { getGlobalConfig } from '../../config/global';
import { TEMPORARY_ERROR } from '../../constants/error-messages'; import { TEMPORARY_ERROR } from '../../constants/error-messages';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { generateInstallCommands } from './buildpack';
import { import {
DockerOptions, DockerOptions,
ExecResult, ExecResult,
...@@ -12,6 +13,7 @@ import { ...@@ -12,6 +13,7 @@ import {
} from './common'; } from './common';
import { generateDockerCommand, removeDockerContainer } from './docker'; import { generateDockerCommand, removeDockerContainer } from './docker';
import { getChildProcessEnv } from './env'; import { getChildProcessEnv } from './env';
import type { ToolConstraint } from './types';
type ExtraEnv<T = unknown> = Record<string, T>; type ExtraEnv<T = unknown> = Record<string, T>;
...@@ -19,6 +21,7 @@ export interface ExecOptions extends ChildProcessExecOptions { ...@@ -19,6 +21,7 @@ export interface ExecOptions extends ChildProcessExecOptions {
cwdFile?: string; cwdFile?: string;
extraEnv?: Opt<ExtraEnv>; extraEnv?: Opt<ExtraEnv>;
docker?: Opt<DockerOptions>; docker?: Opt<DockerOptions>;
toolConstraints?: Opt<ToolConstraint[]>;
} }
function getChildEnv({ function getChildEnv({
...@@ -69,6 +72,7 @@ function getRawExecOptions(opts: ExecOptions): RawExecOptions { ...@@ -69,6 +72,7 @@ function getRawExecOptions(opts: ExecOptions): RawExecOptions {
delete execOptions.extraEnv; delete execOptions.extraEnv;
delete execOptions.docker; delete execOptions.docker;
delete execOptions.cwdFile; delete execOptions.cwdFile;
delete execOptions.toolConstraints;
const childEnv = getChildEnv(opts); const childEnv = getChildEnv(opts);
const cwd = getCwd(opts); const cwd = getCwd(opts);
...@@ -113,7 +117,10 @@ async function prepareRawExec( ...@@ -113,7 +117,10 @@ async function prepareRawExec(
const envVars = dockerEnvVars(extraEnv, childEnv); const envVars = dockerEnvVars(extraEnv, childEnv);
const cwd = getCwd(opts); const cwd = getCwd(opts);
const dockerOptions: DockerOptions = { ...docker, cwd, envVars }; const dockerOptions: DockerOptions = { ...docker, cwd, envVars };
dockerOptions.preCommands = [
...(await generateInstallCommands(opts.toolConstraints)),
...(dockerOptions.preCommands || []),
];
const dockerCommand = await generateDockerCommand( const dockerCommand = await generateDockerCommand(
rawCommands, rawCommands,
dockerOptions dockerOptions
......
export interface ToolConstraint {
toolName: string;
constraint?: string;
}
export interface ToolConfig {
datasource: string;
depName: string;
versioning: string;
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment