feat(composer): support constraints (#7816)

This commit is contained in:
Michael Kriese 2020-11-26 11:09:16 +01:00 committed by GitHub
parent 3f75bd7c12
commit 37e3f971c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 238 additions and 48 deletions

View file

@ -37,7 +37,8 @@
"behat/behat-bundle": "*",
"behat/mink-bundle": "*",
"behat/sahi-client": "*",
"behat/common-contexts": "*"
"behat/common-contexts": "*",
"composer/composer": "^1.10.0"
},
"scripts": {
"post-install-cmd": [

View file

@ -12,6 +12,7 @@
],
"require": {
"aws/aws-sdk-php": "*",
"composer/composer": "^1.10.0",
"wpackagist-plugin/akismet": "dev-trunk",
"wpackagist-plugin/wordpress-seo": ">=7.0.2",
"wpackagist-theme/hueman": "*"

View file

@ -73,7 +73,7 @@ Array [
exports[`.updateArtifacts() returns null if unchanged 1`] = `
Array [
Object {
"cmd": "composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader",
"cmd": "composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",
@ -121,7 +121,7 @@ Array [
exports[`.updateArtifacts() supports docker mode 1`] = `
Array [
Object {
"cmd": "docker pull renovate/composer",
"cmd": "docker pull renovate/composer:1.10.17",
"options": Object {
"encoding": "utf-8",
},
@ -133,7 +133,7 @@ Array [
},
},
Object {
"cmd": "docker run --rm --name=renovate_composer --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -e COMPOSER_CACHE_DIR -w \\"/tmp/github/some/repo\\" renovate/composer bash -l -c \\"composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader\\"",
"cmd": "docker run --rm --name=renovate_composer --label=renovate_child --user=foobar -v \\"/tmp/github/some/repo\\":\\"/tmp/github/some/repo\\" -v \\"/tmp/renovate/cache\\":\\"/tmp/renovate/cache\\" -e COMPOSER_CACHE_DIR -w \\"/tmp/github/some/repo\\" renovate/composer:1.10.17 bash -l -c \\"composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader\\"",
"options": Object {
"cwd": "/tmp/github/some/repo",
"encoding": "utf-8",

View file

@ -2,6 +2,10 @@
exports[`lib/manager/composer/extract extractPackageFile() extracts dependencies with lock file 1`] = `
Object {
"constraints": Object {
"composer": "^1.10.0",
"php": ">=5.3.2",
},
"deps": Array [
Object {
"currentValue": ">=5.3.2",
@ -197,12 +201,22 @@ Object {
"depType": "require-dev",
"skipReason": "any-version",
},
Object {
"currentValue": "^1.10.0",
"datasource": "packagist",
"depName": "composer/composer",
"depType": "require-dev",
},
],
}
`;
exports[`lib/manager/composer/extract extractPackageFile() extracts dependencies with no lock file 1`] = `
Object {
"constraints": Object {
"composer": "^1.10.0",
"php": ">=5.3.2",
},
"deps": Array [
Object {
"currentValue": ">=5.3.2",
@ -398,12 +412,22 @@ Object {
"depType": "require-dev",
"skipReason": "any-version",
},
Object {
"currentValue": "^1.10.0",
"datasource": "packagist",
"depName": "composer/composer",
"depType": "require-dev",
},
],
}
`;
exports[`lib/manager/composer/extract extractPackageFile() extracts object registryUrls 1`] = `
Object {
"constraints": Object {
"composer": "1.*",
"php": ">=5.5",
},
"deps": Array [
Object {
"currentValue": ">=5.5",
@ -505,6 +529,9 @@ Object {
exports[`lib/manager/composer/extract extractPackageFile() extracts object repositories and registryUrls with lock file 1`] = `
Object {
"constraints": Object {
"composer": "1.*",
},
"deps": Array [
Object {
"currentValue": "*",
@ -539,6 +566,9 @@ Object {
exports[`lib/manager/composer/extract extractPackageFile() extracts registryUrls 1`] = `
Object {
"constraints": Object {
"composer": "^1.10.0",
},
"deps": Array [
Object {
"currentValue": "*",
@ -547,6 +577,12 @@ Object {
"depType": "require",
"skipReason": "any-version",
},
Object {
"currentValue": "^1.10.0",
"datasource": "packagist",
"depName": "composer/composer",
"depType": "require",
},
Object {
"currentValue": "dev-trunk",
"datasource": "packagist",
@ -575,6 +611,9 @@ Object {
exports[`lib/manager/composer/extract extractPackageFile() extracts repositories and registryUrls 1`] = `
Object {
"constraints": Object {
"composer": "1.*",
},
"deps": Array [
Object {
"currentValue": "*",

View file

@ -1,27 +1,28 @@
import { exec as _exec } from 'child_process';
import { join } from 'upath';
import { envMock, mockExecAll } from '../../../test/execUtil';
import { fs, git, mocked } from '../../../test/util';
import { env, fs, git, mocked, partial } from '../../../test/util';
import {
PLATFORM_TYPE_GITHUB,
PLATFORM_TYPE_GITLAB,
} from '../../constants/platforms';
import * as _datasource from '../../datasource';
import * as datasourcePackagist from '../../datasource/packagist';
import { setUtilConfig } from '../../util';
import { BinarySource } from '../../util/exec/common';
import * as docker from '../../util/exec/docker';
import * as _env from '../../util/exec/env';
import { StatusResult } from '../../util/git';
import * as hostRules from '../../util/host-rules';
import * as composer from './artifacts';
jest.mock('child_process');
jest.mock('../../util/exec/env');
jest.mock('../../../lib/datasource');
jest.mock('../../util/fs');
jest.mock('../../util/git');
const exec: jest.Mock<typeof _exec> = _exec as any;
const env = mocked(_env);
const datasource = mocked(_datasource);
const config = {
// `join` fixes Windows CI
@ -31,6 +32,12 @@ const config = {
composerIgnorePlatformReqs: true,
};
const repoStatus = partial<StatusResult>({
modified: [],
not_added: [],
deleted: [],
});
describe('.updateArtifacts()', () => {
beforeEach(async () => {
jest.resetAllMocks();
@ -39,6 +46,7 @@ describe('.updateArtifacts()', () => {
await setUtilConfig(config);
docker.resetPrefetchedImages();
hostRules.clear();
delete global.trustLevel;
});
it('returns if no composer.lock found', async () => {
expect(
@ -54,7 +62,8 @@ describe('.updateArtifacts()', () => {
fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('Current composer.lock' as any);
git.getRepoStatus.mockResolvedValue({ modified: [] } as StatusResult);
git.getRepoStatus.mockResolvedValue(repoStatus);
global.trustLevel = 'high';
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
@ -100,7 +109,7 @@ describe('.updateArtifacts()', () => {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValue({ modified: [] } as StatusResult);
git.getRepoStatus.mockResolvedValue(repoStatus);
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
@ -116,8 +125,9 @@ describe('.updateArtifacts()', () => {
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
git.getRepoStatus.mockResolvedValue({
...repoStatus,
modified: ['composer.lock'],
} as StatusResult);
});
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
@ -136,10 +146,11 @@ describe('.updateArtifacts()', () => {
fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
const execSnapshots = mockExecAll(exec);
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock', foo],
not_added: [bar],
deleted: [baz],
} as StatusResult);
});
fs.readLocalFile.mockResolvedValueOnce('New composer.lock' as any);
fs.readLocalFile.mockResolvedValueOnce('Foo' as any);
fs.readLocalFile.mockResolvedValueOnce('Bar' as any);
@ -164,8 +175,9 @@ describe('.updateArtifacts()', () => {
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
git.getRepoStatus.mockResolvedValue({
...repoStatus,
modified: ['composer.lock'],
} as StatusResult);
});
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
@ -187,12 +199,26 @@ describe('.updateArtifacts()', () => {
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.10.0' },
{ version: '1.10.17' },
{ version: '2.0.0' },
{ version: '2.0.7' },
],
});
git.getRepoStatus.mockResolvedValue({
...repoStatus,
modified: ['composer.lock'],
});
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
config: { ...config, constraints: { composer: '^1.10.0' } },
})
).not.toBeNull();
expect(execSnapshots).toMatchSnapshot();
@ -201,6 +227,10 @@ describe('.updateArtifacts()', () => {
fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
git.getRepoStatus.mockResolvedValue({
...repoStatus,
modified: ['composer.lock'],
});
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',
@ -265,8 +295,9 @@ describe('.updateArtifacts()', () => {
const execSnapshots = mockExecAll(exec);
fs.readLocalFile.mockReturnValueOnce('New composer.lock' as any);
git.getRepoStatus.mockResolvedValue({
...repoStatus,
modified: ['composer.lock'],
} as StatusResult);
});
expect(
await composer.updateArtifacts({
packageFileName: 'composer.json',

View file

@ -22,6 +22,7 @@ import {
import { getRepoStatus } from '../../util/git';
import * as hostRules from '../../util/host-rules';
import { UpdateArtifact, UpdateArtifactsResult } from '../common';
import { composerVersioningId, getConstraint } from './utils';
interface UserPass {
username: string;
@ -127,6 +128,8 @@ export async function updateArtifacts({
},
docker: {
image: 'renovate/composer',
tagConstraint: getConstraint(config),
tagScheme: composerVersioningId,
},
};
const cmd = 'composer';
@ -161,7 +164,7 @@ export async function updateArtifacts({
},
];
for (const f of status.modified.concat(status.not_added)) {
for (const f of [...status.modified, ...status.not_added]) {
if (f.startsWith(vendorDir)) {
res.push({
file: {
@ -171,7 +174,7 @@ export async function updateArtifacts({
});
}
}
for (const f of status.deleted || []) {
for (const f of status.deleted) {
res.push({
file: {
name: '|delete|',

View file

@ -6,28 +6,8 @@ import { SkipReason } from '../../types';
import { readLocalFile } from '../../util/fs';
import { api as semverComposer } from '../../versioning/composer';
import { PackageDependency, PackageFile } from '../common';
interface Repo {
name?: string;
type: 'composer' | 'git' | 'package' | 'vcs';
packagist?: boolean;
'packagist.org'?: boolean;
url: string;
}
interface ComposerConfig {
type?: string;
/**
* A repositories field can be an array of Repo objects or an object of repoName: Repo
* Also it can be a boolean (usually false) to disable packagist.
* (Yes this can be confusing, as it is also not properly documented in the composer docs)
* See https://getcomposer.org/doc/05-repositories.md#disabling-packagist-org
*/
repositories: Record<string, Repo | boolean> | Repo[];
require: Record<string, string>;
'require-dev': Record<string, string>;
}
import type { ComposerConfig, ComposerLock, Repo } from './types';
import { extractContraints } from './utils';
/**
* The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies
@ -75,10 +55,8 @@ function parseRepositories(
if (repo.packagist === false || repo['packagist.org'] === false) {
packagist = false;
}
} else if (
['packagist', 'packagist.org'].includes(key) &&
repo === false
) {
} // istanbul ignore else: invalid repo
else if (['packagist', 'packagist.org'].includes(key) && repo === false) {
packagist = false;
}
});
@ -114,11 +92,11 @@ export async function extractPackageFile(
// handle lockfile
const lockfilePath = fileName.replace(/\.json$/, '.lock');
const lockContents = await readLocalFile(lockfilePath, 'utf8');
let lockParsed;
let lockParsed: ComposerLock;
if (lockContents) {
logger.debug({ packageFile: fileName }, 'Found composer lock file');
try {
lockParsed = JSON.parse(lockContents);
lockParsed = JSON.parse(lockContents) as ComposerLock;
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error processing composer.lock');
}
@ -131,6 +109,9 @@ export async function extractPackageFile(
if (registryUrls.length !== 0) {
res.registryUrls = registryUrls;
}
res.constraints = extractContraints(composerJson, lockParsed);
const deps = [];
const depTypes = ['require', 'require-dev'];
for (const depType of depTypes) {
@ -172,8 +153,10 @@ export async function extractPackageFile(
}
if (lockParsed) {
const lockField =
depType === 'require' ? 'packages' : 'packages-dev';
const lockedDep = lockParsed?.[lockField]?.find(
depType === 'require'
? 'packages'
: /* istanbul ignore next */ 'packages-dev';
const lockedDep = lockParsed[lockField]?.find(
(item) => item.name === dep.depName
);
if (lockedDep && semverComposer.isVersion(lockedDep.version)) {

View file

@ -1,8 +1,8 @@
import { LANGUAGE_PHP } from '../../constants/languages';
import * as composerVersioning from '../../versioning/composer';
import { updateArtifacts } from './artifacts';
import { extractPackageFile } from './extract';
import { getRangeStrategy } from './range';
import { composerVersioningId } from './utils';
const language = LANGUAGE_PHP;
export const supportsLockFileMaintenance = true;
@ -11,5 +11,5 @@ export { extractPackageFile, updateArtifacts, language, getRangeStrategy };
export const defaultConfig = {
fileMatch: ['(^|/)([\\w-]*)composer.json$'],
versioning: composerVersioning.id,
versioning: composerVersioningId,
};

View file

@ -0,0 +1,32 @@
// istanbul ignore file: types only
export interface Repo {
name?: string;
type: 'composer' | 'git' | 'package' | 'vcs';
packagist?: boolean;
'packagist.org'?: boolean;
url: string;
}
export interface ComposerConfig {
type?: string;
/**
* A repositories field can be an array of Repo objects or an object of repoName: Repo
* Also it can be a boolean (usually false) to disable packagist.
* (Yes this can be confusing, as it is also not properly documented in the composer docs)
* See https://getcomposer.org/doc/05-repositories.md#disabling-packagist-org
*/
repositories?: Record<string, Repo | boolean> | Repo[];
require?: Record<string, string>;
'require-dev'?: Record<string, string>;
}
export interface ComposerLockPackage {
name: string;
version: string;
}
export interface ComposerLock {
'plugin-api-version'?: string;
packages?: ComposerLockPackage[];
'packages-dev'?: ComposerLockPackage[];
}

View file

@ -0,0 +1,52 @@
import { getName } from '../../../test/util';
import { extractContraints, getConstraint } from './utils';
describe(getName(__filename), () => {
describe('getConstraint', () => {
it('returns from config', () => {
expect(getConstraint({ constraints: { composer: '1.1.0' } })).toEqual(
'1.1.0'
);
});
it('returns from null', () => {
expect(getConstraint({})).toBeNull();
});
});
describe('extractContraints', () => {
it('returns from require', () => {
expect(
extractContraints(
{ require: { php: '>=5.3.2', 'composer/composer': '1.1.0' } },
{}
)
).toEqual({ php: '>=5.3.2', composer: '1.1.0' });
});
it('returns from require-dev', () => {
expect(
extractContraints(
{ 'require-dev': { 'composer/composer': '1.1.0' } },
{}
)
).toEqual({ composer: '1.1.0' });
});
it('returns from composer-runtime-api', () => {
expect(
extractContraints({ require: { 'composer-runtime-api': '^1.1.0' } }, {})
).toEqual({ composer: '1.*' });
});
it('returns from plugin-api-version', () => {
expect(extractContraints({}, { 'plugin-api-version': '1.1.0' })).toEqual({
composer: '1.*',
});
});
it('fallback to 1.*', () => {
expect(extractContraints({}, {})).toEqual({ composer: '1.*' });
});
});
});

View file

@ -0,0 +1,48 @@
import { logger } from '../../logger';
import { api, id as composerVersioningId } from '../../versioning/composer';
import { UpdateArtifactsConfig } from '../common';
import type { ComposerConfig, ComposerLock } from './types';
export { composerVersioningId };
export function getConstraint(config: UpdateArtifactsConfig): string {
const { constraints = {} } = config;
const { composer } = constraints;
if (composer) {
logger.debug('Using composer constraint from config');
return composer;
}
return null;
}
export function extractContraints(
composerJson: ComposerConfig,
lockParsed: ComposerLock
): Record<string, string> {
const res: Record<string, string> = { composer: '1.*' };
// extract php
if (composerJson.require?.php) {
res.php = composerJson.require?.php;
}
// extract direct composer dependency
if (composerJson.require?.['composer/composer']) {
res.composer = composerJson.require?.['composer/composer'];
} else if (composerJson['require-dev']?.['composer/composer']) {
res.composer = composerJson['require-dev']?.['composer/composer'];
}
// check last used composer version
else if (lockParsed?.['plugin-api-version']) {
const major = api.getMajor(lockParsed?.['plugin-api-version']);
res.composer = `${major}.*`;
}
// check composer api dependency
else if (composerJson.require?.['composer-runtime-api']) {
const major = api.getMajor(composerJson.require?.['composer-runtime-api']);
res.composer = `${major}.*`;
}
return res;
}