diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts index 929d64b588ef0b..27a5378f624fa3 100644 --- a/lib/manager/composer/artifacts.ts +++ b/lib/manager/composer/artifacts.ts @@ -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, @@ -25,7 +26,6 @@ import { composerVersioningId, extractContraints, getComposerArguments, - getComposerConstraint, getPhpConstraint, } from './utils'; @@ -102,9 +102,10 @@ 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, @@ -112,8 +113,8 @@ export async function updateArtifacts({ COMPOSER_CACHE_DIR: await ensureCacheDir('composer'), COMPOSER_AUTH: getAuthJson(), }, + toolConstraints: [composerToolConstraint], docker: { - preCommands, image: 'php', tagConstraint: getPhpConstraint(constraints), tagScheme: composerVersioningId, diff --git a/lib/manager/composer/utils.spec.ts b/lib/manager/composer/utils.spec.ts index edeab175287343..6970039a65ffde 100644 --- a/lib/manager/composer/utils.spec.ts +++ b/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( diff --git a/lib/manager/composer/utils.ts b/lib/manager/composer/utils.ts index 57b837dc703c81..68684d9ba7cd1c 100644 --- a/lib/manager/composer/utils.ts +++ b/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'; @@ -29,45 +28,6 @@ export function getComposerArguments(config: UpdateArtifactsConfig): string { return args; } -export async function getComposerConstraint( - constraints: Record -): Promise { - 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 { const { php } = constraints; diff --git a/lib/util/exec/buildpack.spec.ts b/lib/util/exec/buildpack.spec.ts new file mode 100644 index 00000000000000..1a47e3f3c45ac6 --- /dev/null +++ b/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", + ] + `); + }); + }); +}); diff --git a/lib/util/exec/buildpack.ts b/lib/util/exec/buildpack.ts new file mode 100644 index 00000000000000..ae47f44e8cc6d1 --- /dev/null +++ b/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 = { + composer: { + datasource: 'github-releases', + depName: 'composer/composer', + versioning: composerVersioningId, + }, +}; + +export async function resolveConstraint( + toolConstraint: ToolConstraint +): Promise { + 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 { + 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; +} diff --git a/lib/util/exec/index.ts b/lib/util/exec/index.ts index b086f22b1b4fee..c093e0b059e3b4 100644 --- a/lib/util/exec/index.ts +++ b/lib/util/exec/index.ts @@ -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, @@ -12,6 +13,7 @@ import { } from './common'; import { generateDockerCommand, removeDockerContainer } from './docker'; import { getChildProcessEnv } from './env'; +import type { ToolConstraint } from './types'; type ExtraEnv = Record; @@ -19,6 +21,7 @@ export interface ExecOptions extends ChildProcessExecOptions { cwdFile?: string; extraEnv?: Opt; docker?: Opt; + toolConstraints?: Opt; } function getChildEnv({ @@ -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); @@ -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 diff --git a/lib/util/exec/types.ts b/lib/util/exec/types.ts new file mode 100644 index 00000000000000..65b015083da0f0 --- /dev/null +++ b/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; +}