Files
renovate/lib/modules/manager/composer/artifacts.spec.ts
Sebastian Poxhofer 4b4d14ec00 feat(manager): pass extracted constraints to updateArtifacts (#45826)
* feat(manager): pass extracted constraints to updateArtifacts

`extractedConstraints` were collected during extraction but only ever read
by `fetch.ts` for the lookup. At artifact time every manager had to derive
the same tool constraints a second time, and when the updated package file
no longer carried the constraint there was nothing to fall back to.

`generateBranchConfig()` now merges the `extractedConstraints` of all
upgrades of a branch onto the branch config, which reaches `updateArtifacts()`
through `patchConfigForArtifactsUpdate()`. `UpdateArtifactsConfig` declares
the field and documents what it contains.

The new `resolveToolConstraint()` helper implements the ladder once:
user `constraints` > an optional artifact-time derivation > the extracted
value. The derivation is ranked above the extracted value on purpose: it
reads the package files as the branch changed them, so poetry (python is an
updatable dependency there), composer, pipenv (`pipenv` can be a dev
package) and pep621 (`requires-python` is an updatable dependency) would
otherwise regress to a stale constraint. pipenv, pep621 uv/pdm, poetry and
composer are migrated to the helper and keep their derivation.

Behaviour change: these managers now use the extract-time constraint as a
last resort instead of running unconstrained, for example on lock file
maintenance where pipenv deletes `Pipfile.lock` before deriving.

`execute-post-upgrade-commands.ts` is unaffected: it reads
`upgrade.constraints` per upgrade, and the extracted values are kept in a
separate field rather than folded into `constraints`, so post-upgrade task
tool constraints still come from user config only.

Not migrated, because their constraints are never extracted, so the
fallback would be dead code: bundler (`ruby`/`bundler`), pixi (`pixi`),
pep621 uv (`required-version`) and npm's node ladder, whose last step
already reads `package.json` engines.

* refactor(manager): resolve tool constraints through resolveToolConstraint()

Every manager now obtains the constraint of a tool it runs through
`resolveToolConstraint()` instead of reading `config.constraints` itself, so
the precedence "user config, then a value derived from the updated files, then
the constraints collected during extraction" applies everywhere. The helper
treats an empty string as not set at every step and accepts the
`PostUpdateConfig` shape, whose `constraints` may be `null`.

Behaviour changes that go beyond the new fallback to extracted constraints:

- `mix` applies its hardcoded erlang default only after the extracted
  constraint, so an erlang version collected at extraction can win over it.
- `kustomize` decides `HELM_EXPERIMENTAL_OCI` from the fully resolved helm
  constraint; `generateHelmEnvs()` takes that value instead of the config.
- `npm` (yarn): an upgrade of yarn without a new value now consults the user
  constraint before deriving one from package.json or the lock file.
- The "Using <tool> constraint from config" debug lines in bundler, composer,
  copier and pip-compile are gone, as the helper is the only reader of the
  config now.

Helpers that read the config synchronously became async: bundler
`getBundlerConstraint()`, copier `getPythonVersionConstraint()` and
`getCopierVersionConstraint()`, pip-compile `get*VersionConstraint()` and
`getToolVersionConstraint()`, gomod `deriveGoToolchainConstraints()` and
`getUpdateImportPathCmds()`, mise `getMiseLockToolConstraints()`. composer
`getPhpConstraint()` is removed.

Each converted manager has a test showing the extracted constraint reaching
the executed tool, and, where a derive callback exists, that the derived value
still wins over it.

* refactor(linters): add prefer-resolve-tool-constraint rule

Reports member reads and destructuring of `constraints` and
`extractedConstraints` from a manager config in `lib/modules/manager/**`, so
that tool constraints are resolved through `resolveToolConstraint()` in
`lib/modules/manager/util.ts` only. Writes and the same properties on package
files, which extraction has to populate, are not reported. The helper itself
and spec files are exempt in `.oxlintrc.json`.

* refactor(manager): inline thin resolveToolConstraint wrappers

Drop copier/utils.ts and the pip-compile getPipToolsVersionConstraint /
getUvVersionConstraint helpers, which only forwarded to
resolveToolConstraint(). Callers now call it directly.

* docs(development): note lock file maintenance in tool constraints section

* refactor(manager): address review on tool constraint resolution

composer only runs extractConstraints() when a user constraint is missing,
via Lazy. UpdateArtifactsConfig no longer re-declares constraints and
extractedConstraints, which ToolConstraintsConfig already provides.
2026-09-14 11:09:07 +00:00

1313 lines
39 KiB
TypeScript

import upath from 'upath';
import { mockDeep } from 'vitest-mock-extended';
import { envMock, mockExecAll } from '~test/exec-util.ts';
import { env, fs, git, partial } from '~test/util.ts';
import { GlobalConfig } from '../../../config/global.ts';
import type {
InternalGlobalConfigOptions,
RepoGlobalConfig,
} from '../../../config/types.ts';
import * as docker from '../../../util/exec/docker/index.ts';
import type { StatusResult } from '../../../util/git/types.ts';
import * as hostRules from '../../../util/host-rules.ts';
import { GitTagsDatasource } from '../../datasource/git-tags/index.ts';
import * as _datasource from '../../datasource/index.ts';
import { PackagistDatasource } from '../../datasource/packagist/index.ts';
import type { UpdateArtifactsConfig } from '../types.ts';
import * as composer from './index.ts';
vi.mock('../../../util/exec/env.ts');
vi.mock('../../datasource/index.ts', () => mockDeep());
vi.mock('../../../util/fs/index.ts');
process.env.CONTAINERBASE = 'true';
const datasource = vi.mocked(_datasource);
const config: UpdateArtifactsConfig = {
composerIgnorePlatformReqs: [],
ignoreScripts: false,
};
const adminConfig: RepoGlobalConfig & InternalGlobalConfigOptions = {
allowPlugins: false,
allowScripts: false,
// `join` fixes Windows CI
localDir: upath.join('/tmp/github/some/repo'),
cacheDir: upath.join('/tmp/renovate/cache'),
containerbaseDir: upath.join('/tmp/renovate/cache/containerbase'),
dockerSidecarImage: 'ghcr.io/renovatebot/base-image',
binarySource: 'global',
};
const repoStatus = partial<StatusResult>({
modified: [],
not_added: [],
deleted: [],
});
describe('modules/manager/composer/artifacts', () => {
beforeEach(() => {
env.getChildProcessEnv.mockReturnValue(envMock.basic);
docker.resetPrefetchedImages();
hostRules.clear();
GlobalConfig.set(adminConfig);
fs.ensureCacheDir.mockResolvedValue('/tmp/renovate/cache/others/composer');
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '1.0.0' },
{ version: '1.1.0' },
{ version: '1.3.0' },
{ version: '1.10.0' },
{ version: '1.10.17' },
{ version: '2.0.14' },
{ version: '2.1.0' },
],
});
});
afterEach(() => {
GlobalConfig.reset();
});
it('returns if no composer.lock found', async () => {
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).resolves.toBeNull();
});
it('returns null if unchanged', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
GlobalConfig.set({
...adminConfig,
allowScripts: true,
allowPlugins: true,
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [
{ depName: 'foo', newVersion: '1.0.0' },
{ depName: 'bar', newVersion: '2.0.0' },
],
newPackageFileContent: '{}',
config,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update foo:1.0.0 bar:2.0.0 --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction',
options: {
cwd: '/tmp/github/some/repo',
env: {
COMPOSER_CACHE_DIR: '/tmp/renovate/cache/others/composer',
},
},
},
]);
});
it('uses hostRules to set COMPOSER_AUTH', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'api.github.com',
token: 'ghp_github-token',
});
// This rule should not affect the result the Github rule has priority to avoid breaking changes.
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_git-tags-token',
});
hostRules.add({
hostType: 'gitlab',
matchHost: 'gitlab.com',
token: 'gitlab-token',
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'packagist.renovatebot.com',
username: 'some-username',
password: 'some-password',
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://artifactory.yyyyyyy.com/artifactory/api/composer/',
username: 'some-other-username',
password: 'some-other-password',
});
hostRules.add({
hostType: PackagistDatasource.id,
username: 'some-other-username',
password: 'some-other-password',
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://packages-bearer.example.com/',
token: 'abcdef0123456789',
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: {
cwd: '/tmp/github/some/repo',
env: {
COMPOSER_AUTH:
'{"github-oauth":{"github.com":"ghp_git-tags-token"},' +
'"gitlab-token":{"gitlab.com":"gitlab-token"},' +
'"gitlab-domains":["gitlab.com"],' +
'"http-basic":{' +
'"packagist.renovatebot.com":{"username":"some-username","password":"some-password"},' +
'"artifactory.yyyyyyy.com":{"username":"some-other-username","password":"some-other-password"}' +
'},' +
'"bearer":{"packages-bearer.example.com":"abcdef0123456789"}}',
COMPOSER_CACHE_DIR: '/tmp/renovate/cache/others/composer',
},
},
},
]);
});
it('git-tags hostRule for github.com set github-token in COMPOSER_AUTH', async () => {
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('Skip github application access token hostRules in COMPOSER_AUTH', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'api.github.com',
token: 'ghs_token',
});
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('github hostRule for github.com with x-access-token set github-token in COMPOSER_AUTH', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'https://api.github.com/',
token: 'x-access-token:ghp_token',
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('does set github COMPOSER_AUTH for github when only hostType git-tags artifactAuth does not include composer', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'api.github.com',
token: 'ghs_token',
});
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
artifactAuth: [],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghs_token"}}',
},
},
},
]);
});
it('does set github COMPOSER_AUTH for git-tags when only hostType github artifactAuth does not include composer', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'api.github.com',
token: 'ghs_token',
artifactAuth: [],
});
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('does not set github COMPOSER_AUTH when artifactAuth does not include composer, for both hostType github & git-tags', async () => {
hostRules.add({
hostType: 'github',
matchHost: 'api.github.com',
token: 'ghs_token',
artifactAuth: [],
});
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
artifactAuth: [],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots[0].options?.env).not.toContainKey('COMPOSER_AUTH');
});
it('does not set gitlab COMPOSER_AUTH when artifactAuth does not include composer', async () => {
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
hostRules.add({
hostType: 'gitlab',
matchHost: 'gitlab.com',
token: 'gitlab-token',
artifactAuth: [],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
postUpdateOptions: ['composerGitlabToken'],
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('does not set packagist COMPOSER_AUTH when artifactAuth does not include composer', async () => {
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'packagist.renovatebot.com',
username: 'some-username',
password: 'some-password',
artifactAuth: [],
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://artifactory.yyyyyyy.com/artifactory/api/composer/',
username: 'some-other-username',
password: 'some-other-password',
artifactAuth: [],
});
hostRules.add({
hostType: PackagistDatasource.id,
username: 'some-other-username',
password: 'some-other-password',
artifactAuth: [],
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://packages-bearer.example.com/',
token: 'abcdef0123456789',
artifactAuth: [],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
postUpdateOptions: ['composerGitlabToken'],
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}',
},
},
},
]);
});
it('does set gitlab COMPOSER_AUTH when artifactAuth does include composer', async () => {
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
hostRules.add({
hostType: 'gitlab',
matchHost: 'gitlab.com',
token: 'gitlab-token',
artifactAuth: ['composer'],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
postUpdateOptions: ['composerGitlabToken'],
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH:
'{"github-oauth":{"github.com":"ghp_token"},' +
'"gitlab-token":{"gitlab.com":"gitlab-token"},' +
'"gitlab-domains":["gitlab.com"]}',
},
},
},
]);
});
it('does set packagist COMPOSER_AUTH when artifactAuth does include composer', async () => {
hostRules.add({
hostType: GitTagsDatasource.id,
matchHost: 'github.com',
token: 'ghp_token',
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'packagist.renovatebot.com',
username: 'some-username',
password: 'some-password',
artifactAuth: ['composer'],
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://artifactory.yyyyyyy.com/artifactory/api/composer/',
username: 'some-other-username',
password: 'some-other-password',
artifactAuth: ['composer'],
});
hostRules.add({
hostType: PackagistDatasource.id,
username: 'some-other-username',
password: 'some-other-password',
artifactAuth: ['composer'],
});
hostRules.add({
hostType: PackagistDatasource.id,
matchHost: 'https://packages-bearer.example.com/',
token: 'abcdef0123456789',
artifactAuth: ['composer'],
});
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
const authConfig = {
...config,
postUpdateOptions: ['composerGitlabToken'],
registryUrls: ['https://packagist.renovatebot.com'],
};
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: authConfig,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
options: {
env: {
COMPOSER_AUTH:
'{"github-oauth":{"github.com":"ghp_token"},' +
'"http-basic":{' +
'"packagist.renovatebot.com":{"username":"some-username","password":"some-password"},' +
'"artifactory.yyyyyyy.com":{"username":"some-other-username","password":"some-other-password"}' +
'},' +
'"bearer":{"packages-bearer.example.com":"abcdef0123456789"}}',
},
},
},
]);
});
it('returns updated composer.lock', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).resolves.toEqual([
{
file: {
contents: '{}',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('supports vendor directory update', async () => {
const foo = upath.join('vendor/foo/Foo.php');
const bar = upath.join('vendor/bar/Bar.php');
const baz = upath.join('vendor/baz/Baz.php');
fs.localPathExists.mockResolvedValueOnce(true);
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock', foo],
not_added: [bar],
deleted: [baz],
});
fs.readLocalFile.mockResolvedValueOnce('{ }');
fs.readLocalFile.mockResolvedValueOnce('Foo');
fs.readLocalFile.mockResolvedValueOnce('Bar');
fs.getSiblingFileName.mockReturnValueOnce('vendor');
const res = await composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
});
expect(res).toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
{
file: {
contents: 'Foo',
path: 'vendor/foo/Foo.php',
type: 'addition',
},
},
{
file: {
contents: 'Bar',
path: 'vendor/bar/Bar.php',
type: 'addition',
},
},
{
file: {
path: 'vendor/baz/Baz.php',
type: 'deletion',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('performs lockFileMaintenance', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: {
...config,
isLockFileMaintenance: true,
},
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(fs.deleteLocalFile).toHaveBeenCalledTimes(1);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('supports docker mode', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'docker' });
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '7.2.34' },
{ version: '7.3' }, // composer versioning bug
{ version: '7.3.29' },
{ version: '7.4.22' },
{ version: '8.0.6' },
],
});
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: { ...config, constraints: { composer: '^1.10.0', php: '7.3' } },
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'docker pull ghcr.io/renovatebot/base-image',
options: {},
},
{
cmd: 'docker ps --filter name=renovate_sidecar -aq',
options: {},
},
{
cmd:
'docker run --rm --name=renovate_sidecar --label=renovate_child ' +
'-v "/tmp/github/some/repo":"/tmp/github/some/repo" ' +
'-v "/tmp/renovate/cache":"/tmp/renovate/cache" ' +
'-e CI ' +
'-e COMPOSER_CACHE_DIR ' +
'-e CONTAINERBASE_CACHE_DIR ' +
'-w "/tmp/github/some/repo" ' +
'ghcr.io/renovatebot/base-image' +
" bash -l -c '" +
'install-tool php 7.3' +
' && ' +
'install-tool composer 1.10.17' +
' && ' +
'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins' +
"'",
options: {
cwd: '/tmp/github/some/repo',
env: {
COMPOSER_CACHE_DIR: '/tmp/renovate/cache/others/composer',
},
},
},
]);
});
it('supports install mode', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
datasource.getPkgReleases.mockResolvedValueOnce({
releases: [
{ version: '7.2.34' },
{ version: '7.3' }, // composer versioning bug
{ version: '7.3.29' },
{ version: '7.4.22' },
{ version: '8.0.6' },
],
});
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: { ...config, constraints: { composer: '^1.10.0', php: '7.3' } },
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'install-tool php 7.3',
},
{
cmd: 'install-tool composer 1.10.17',
},
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: {
cwd: '/tmp/github/some/repo',
env: {
COMPOSER_CACHE_DIR: '/tmp/renovate/cache/others/composer',
},
},
},
]);
});
it('falls back to the extracted php constraint', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
// the updated `composer.json` requires no php version itself
newPackageFileContent: '{}',
config: {
...config,
extractedConstraints: { php: '7.3' },
constraints: { composer: '^1.10.0' },
},
}),
).resolves.not.toBeNull();
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool php 7.3' },
{ cmd: 'install-tool composer 1.10.17' },
{ cmd: expect.stringContaining('composer update') },
]);
});
it('prefers the php constraint of the updated package file over the extracted one', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'install' });
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{"require":{"php":"7.4"}}',
config: {
...config,
extractedConstraints: { php: '7.3' },
constraints: { composer: '^1.10.0' },
},
}),
).resolves.not.toBeNull();
expect(execSnapshots).toMatchObject([
{ cmd: 'install-tool php 7.4' },
{ cmd: 'install-tool composer 1.10.17' },
{ cmd: expect.stringContaining('composer update') },
]);
});
it('supports global mode', async () => {
GlobalConfig.set({ ...adminConfig, binarySource: 'global' });
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('catches errors', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
fs.writeLocalFile.mockImplementationOnce(() => {
throw new Error('not found');
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).resolves.toEqual([
{
artifactError: {
fileName: 'composer.lock',
stderr: 'not found',
},
},
]);
expect(execSnapshots).toBeEmptyArray();
});
it('catches unmet requirements errors', async () => {
const execSnapshots = mockExecAll();
const stderr =
'fooYour requirements could not be resolved to an installable set of packages.bar';
fs.readLocalFile.mockResolvedValueOnce('{}');
fs.writeLocalFile.mockImplementationOnce(() => {
throw new Error(stderr);
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).resolves.toEqual([
{ artifactError: { fileName: 'composer.lock', stderr } },
]);
expect(execSnapshots).toBeEmptyArray();
});
it('throws for disk space', async () => {
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
fs.writeLocalFile.mockImplementationOnce(() => {
throw new Error(
'vendor/composer/07fe2366/sebastianbergmann-php-code-coverage-c896779/src/Report/Html/Renderer/Template/js/d3.min.js: write error (disk full?). Continue? (y/n/^C) ',
);
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config,
}),
).rejects.toThrow('disk-space');
expect(execSnapshots).toBeEmptyArray();
});
it('disables ignorePlatformReqs', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: {
...config,
composerIgnorePlatformReqs: undefined,
},
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('adds all ignorePlatformReq items', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: {
...config,
composerIgnorePlatformReqs: ['ext-posix', 'ext-sodium'],
},
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update --with-dependencies --ignore-platform-req ext-posix --ignore-platform-req ext-sodium --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('installs before running the update when symfony flex is installed', async () => {
fs.readLocalFile.mockResolvedValueOnce(
'{"packages":[{"name":"symfony/flex","version":"1.17.1"}]}',
);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: {
...config,
},
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'git stash -- composer.json',
},
{
cmd: 'composer install --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
{
cmd: {
command: ['git', 'stash', 'pop'],
ignoreFailure: true,
},
},
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('installs before running the update when symfony flex is installed as dev', async () => {
fs.readLocalFile.mockResolvedValueOnce(
'{"packages-dev":[{"name":"symfony/flex","version":"1.17.1"}]}',
);
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{ }');
git.getRepoStatus.mockResolvedValueOnce({
...repoStatus,
modified: ['composer.lock'],
});
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [],
newPackageFileContent: '{}',
config: {
...config,
},
}),
).resolves.toEqual([
{
file: {
contents: '{ }',
path: 'composer.lock',
type: 'addition',
},
},
]);
expect(execSnapshots).toMatchObject([
{
cmd: 'git stash -- composer.json',
},
{
cmd: 'composer install --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
{
cmd: {
command: ['git', 'stash', 'pop'],
ignoreFailure: true,
},
},
{
cmd: 'composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('does not disable plugins when configured globally', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
GlobalConfig.set({ ...adminConfig, allowPlugins: true });
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [
{ depName: 'foo', newVersion: '1.0.0' },
{ depName: 'bar', newVersion: '2.0.0' },
],
newPackageFileContent: '{}',
config,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update foo:1.0.0 bar:2.0.0 --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('disable plugins when configured locally', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
GlobalConfig.set({ ...adminConfig, allowPlugins: true });
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [{ depName: 'foo' }, { depName: 'bar' }],
newPackageFileContent: '{}',
config: {
...config,
ignorePlugins: true,
},
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update foo bar --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('includes new dependency version in update command', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [{ depName: 'foo', newVersion: '1.1.0' }],
newPackageFileContent: '{}',
config,
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update foo:1.1.0 --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
it('uses --with-all-dependencies instead of --with-dependencies when composerUpdateAllDependencies is set in postUpdateOptions', async () => {
fs.readLocalFile.mockResolvedValueOnce('{}');
const execSnapshots = mockExecAll();
fs.readLocalFile.mockResolvedValueOnce('{}');
git.getRepoStatus.mockResolvedValueOnce(repoStatus);
await expect(
composer.updateArtifacts({
packageFileName: 'composer.json',
updatedDeps: [{ depName: 'foo', newVersion: '1.1.0' }],
newPackageFileContent: '{}',
config: {
...config,
postUpdateOptions: ['composerWithAll'],
},
}),
).resolves.toBeNull();
expect(execSnapshots).toMatchObject([
{
cmd: 'composer update foo:1.1.0 --with-all-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins',
options: { cwd: '/tmp/github/some/repo' },
},
]);
});
});