mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-02 02:29:51 +00:00
afd16f7f7f
* docs(manager/circleci): document YAML merge key parsing limitation Signed-off-by: Ethan Setnik <esetnik@gmail.com> * fix(manager/circleci): parse config files as YAML 1.1 CircleCI processes config files as YAML 1.1, but Renovate parsed them as YAML 1.2. Configs that use YAML 1.1 features failed to parse and the entire file was silently skipped, dropping every `docker` and `orb` dep in it. The most common trigger is a mapping with more than one `<<` merge key, which YAML 1.2 rejects as a duplicate key. CircleCI's own processor allows it, since merge keys are a YAML 1.1 feature. Pass `{ version: '1.1' }` to `parseSingleYaml` so the parser matches CircleCI's behavior, and add a regression test that covers the duplicate-merge-key case. Replaces the workaround note added in the previous commit on this branch (per review feedback) — the config-side `%YAML 1.1` directive is no longer needed. --------- Signed-off-by: Ethan Setnik <esetnik@gmail.com>
387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
import { codeBlock } from 'common-tags';
|
|
import { Fixtures } from '~test/fixtures.ts';
|
|
import { extractPackageFile } from './index.ts';
|
|
|
|
const file1 = Fixtures.get('config.yml');
|
|
const file2 = Fixtures.get('config2.yml');
|
|
const file3 = Fixtures.get('config3.yml');
|
|
const file4 = Fixtures.get('config4.yml');
|
|
|
|
describe('modules/manager/circleci/extract', () => {
|
|
describe('extractPackageFile()', () => {
|
|
it('returns null for empty', () => {
|
|
expect(extractPackageFile('nothing here')).toBeNull();
|
|
});
|
|
|
|
it('handles registry alias', () => {
|
|
const src = codeBlock`
|
|
executors:
|
|
my-executor:
|
|
docker:
|
|
- image: quay.io/myName/myPackage:0.6.2
|
|
`;
|
|
|
|
const res = extractPackageFile(src, '.circleci/config.yml', {
|
|
registryAliases: {
|
|
'quay.io': 'my-quay-mirror.registry.com',
|
|
'index.docker.io': 'my-docker-mirror.registry.com',
|
|
},
|
|
});
|
|
|
|
expect(res).toEqual({
|
|
deps: [
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'quay.io/myName/myPackage:{{#if newValue}}{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest: undefined,
|
|
currentValue: '0.6.2',
|
|
datasource: 'docker',
|
|
depName: 'quay.io/myName/myPackage',
|
|
packageName: 'my-quay-mirror.registry.com/myName/myPackage',
|
|
depType: 'docker',
|
|
replaceString: 'quay.io/myName/myPackage:0.6.2',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extracts multiple image and resolves yaml anchors', () => {
|
|
const res = extractPackageFile(file1);
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
datasource: 'docker',
|
|
depName: 'node',
|
|
packageName: 'node',
|
|
depType: 'docker',
|
|
replaceString: 'node',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '4',
|
|
datasource: 'docker',
|
|
depName: 'node',
|
|
packageName: 'node',
|
|
depType: 'docker',
|
|
replaceString: 'node:4',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '6',
|
|
datasource: 'docker',
|
|
depName: 'node',
|
|
packageName: 'node',
|
|
depType: 'docker',
|
|
replaceString: 'node:6',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '8.9.0',
|
|
datasource: 'docker',
|
|
depName: 'node',
|
|
packageName: 'node',
|
|
depType: 'docker',
|
|
replaceString: 'node:8.9.0',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('extracts orbs too', () => {
|
|
const res = extractPackageFile(file2);
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
currentValue: '4.1.0',
|
|
datasource: 'orb',
|
|
depName: 'release-workflows',
|
|
depType: 'orb',
|
|
packageName: 'hutson/library-release-workflows',
|
|
versioning: 'npm',
|
|
},
|
|
{
|
|
datasource: 'orb',
|
|
depName: 'no-version',
|
|
depType: 'orb',
|
|
packageName: 'abc/def',
|
|
versioning: 'npm',
|
|
},
|
|
{
|
|
currentValue: 'volatile',
|
|
datasource: 'orb',
|
|
depName: 'volatile',
|
|
depType: 'orb',
|
|
packageName: 'zzz/zzz',
|
|
versioning: 'npm',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'python',
|
|
packageName: 'python',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'python',
|
|
packageName: 'python',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'python',
|
|
packageName: 'python',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'python',
|
|
packageName: 'python',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:eb6325b75c1c70b4992eaa1bdd29e24e5f14d5324b4714a49f3e67783473214b',
|
|
currentValue: '3-6',
|
|
datasource: 'docker',
|
|
depName: 'pypy',
|
|
packageName: 'pypy',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'pypy:3-6@sha256:eb6325b75c1c70b4992eaa1bdd29e24e5f14d5324b4714a49f3e67783473214b',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest:
|
|
'sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'python',
|
|
packageName: 'python',
|
|
depType: 'docker',
|
|
replaceString:
|
|
'python:3.7@sha256:3870d35b962a943df72d948580fc66ceaaee1c4fbd205930f32e0f0760eb1077',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('extracts image without leading dash', () => {
|
|
const res = extractPackageFile(file3);
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '14.8.0',
|
|
datasource: 'docker',
|
|
depName: 'cimg/node',
|
|
packageName: 'cimg/node',
|
|
depType: 'docker',
|
|
replaceString: 'cimg/node:14.8.0',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '14.8.0',
|
|
datasource: 'docker',
|
|
depName: 'cimg/node',
|
|
packageName: 'cimg/node',
|
|
depType: 'docker',
|
|
replaceString: 'cimg/node:14.8.0',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('extracts and exclude android images', () => {
|
|
expect(
|
|
extractPackageFile(codeBlock`
|
|
jobs:
|
|
build:
|
|
machine:
|
|
image: android:202102-01
|
|
`),
|
|
).toBeNull();
|
|
});
|
|
|
|
it('extracts orbs without jobs', () => {
|
|
const res = extractPackageFile(file4);
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
currentValue: '5.2.0',
|
|
datasource: 'orb',
|
|
depName: 'nodejs',
|
|
depType: 'orb',
|
|
packageName: 'circleci/node',
|
|
versioning: 'npm',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('extracts executors', () => {
|
|
const res = extractPackageFile(codeBlock`
|
|
executors:
|
|
my-executor:
|
|
docker:
|
|
- image: cimg/ruby:3.0.3-browsers
|
|
`);
|
|
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentValue: '3.0.3-browsers',
|
|
datasource: 'docker',
|
|
depName: 'cimg/ruby',
|
|
packageName: 'cimg/ruby',
|
|
depType: 'docker',
|
|
replaceString: 'cimg/ruby:3.0.3-browsers',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('extracts orb definitions', () => {
|
|
const res = extractPackageFile(codeBlock`
|
|
version: 2.1
|
|
|
|
orbs:
|
|
myorb:
|
|
orbs:
|
|
python: circleci/python@2.1.1
|
|
|
|
executors:
|
|
python:
|
|
docker:
|
|
- image: cimg/python:3.9
|
|
|
|
jobs:
|
|
test_image:
|
|
docker:
|
|
- image: cimg/python:3.7
|
|
steps:
|
|
- checkout
|
|
|
|
workflows:
|
|
Test:
|
|
jobs:
|
|
- myorb/test_image
|
|
`);
|
|
|
|
expect(res).toEqual({
|
|
deps: [
|
|
{
|
|
currentValue: '2.1.1',
|
|
datasource: 'orb',
|
|
depName: 'python',
|
|
depType: 'orb',
|
|
packageName: 'circleci/python',
|
|
versioning: 'npm',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest: undefined,
|
|
currentValue: '3.9',
|
|
datasource: 'docker',
|
|
depName: 'cimg/python',
|
|
packageName: 'cimg/python',
|
|
depType: 'docker',
|
|
replaceString: 'cimg/python:3.9',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest: undefined,
|
|
currentValue: '3.7',
|
|
datasource: 'docker',
|
|
depName: 'cimg/python',
|
|
packageName: 'cimg/python',
|
|
depType: 'docker',
|
|
replaceString: 'cimg/python:3.7',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('extracts deps from configs with multiple merge keys per mapping', () => {
|
|
// YAML 1.2 rejects a mapping with two `<<` keys as a duplicate key and
|
|
// the entire file is skipped. CircleCI itself parses these files as
|
|
// YAML 1.1, where merge keys are valid and additive, so Renovate must
|
|
// do the same to avoid silently missing every dep in the file.
|
|
const res = extractPackageFile(codeBlock`
|
|
version: 2.1
|
|
|
|
aliases:
|
|
- &node-base
|
|
docker:
|
|
- image: node:18
|
|
- &node-env
|
|
environment:
|
|
NODE_ENV: production
|
|
|
|
orbs:
|
|
python: circleci/python@2.1.1
|
|
|
|
jobs:
|
|
build:
|
|
<<: *node-base
|
|
<<: *node-env
|
|
steps:
|
|
- checkout
|
|
`);
|
|
|
|
expect(res?.deps).toEqual([
|
|
{
|
|
currentValue: '2.1.1',
|
|
datasource: 'orb',
|
|
depName: 'python',
|
|
depType: 'orb',
|
|
packageName: 'circleci/python',
|
|
versioning: 'npm',
|
|
},
|
|
{
|
|
autoReplaceStringTemplate:
|
|
'{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}',
|
|
currentDigest: undefined,
|
|
currentValue: '18',
|
|
datasource: 'docker',
|
|
depName: 'node',
|
|
packageName: 'node',
|
|
depType: 'docker',
|
|
replaceString: 'node:18',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|