Skip to content

Commit

Permalink
feat(gitlab): support GitLab CI/CD component references (renovatebot#…
Browse files Browse the repository at this point in the history
…26660)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
  • Loading branch information
2 people authored and Ronald van Butselaar committed Feb 2, 2024
1 parent 8793502 commit edea11d
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 8 deletions.
11 changes: 11 additions & 0 deletions lib/modules/manager/gitlabci/common.spec.ts
Expand Up @@ -4,6 +4,7 @@ import type { GitlabPipeline } from '../gitlabci/types';
import { replaceReferenceTags } from '../gitlabci/utils';
import {
filterIncludeFromGitlabPipeline,
isGitlabIncludeComponent,
isGitlabIncludeLocal,
isGitlabIncludeProject,
isNonEmptyObject,
Expand Down Expand Up @@ -61,6 +62,16 @@ describe('modules/manager/gitlabci/common', () => {
});
});

describe('isGitlabIncludeComponent()', () => {
it('returns true if GitlabInclude is GitlabIncludeComponent', () => {
expect(isGitlabIncludeComponent({ component: 'something' })).toBe(true);
});

it('returns false if GitlabInclude is not GitlabIncludeComponent', () => {
expect(isGitlabIncludeComponent(includeLocal)).toBe(false);
});
});

describe('isNonEmptyObject()', () => {
it('returns true if not empty', () => {
expect(isNonEmptyObject({ attribute1: 1 })).toBe(true);
Expand Down
7 changes: 7 additions & 0 deletions lib/modules/manager/gitlabci/common.ts
@@ -1,6 +1,7 @@
import is from '@sindresorhus/is';
import type {
GitlabInclude,
GitlabIncludeComponent,
GitlabIncludeLocal,
GitlabIncludeProject,
GitlabPipeline,
Expand Down Expand Up @@ -32,3 +33,9 @@ export function isGitlabIncludeLocal(
): include is GitlabIncludeLocal {
return !is.undefined((include as GitlabIncludeLocal).local);
}

export function isGitlabIncludeComponent(
include: GitlabInclude,
): include is GitlabIncludeComponent {
return !is.undefined((include as GitlabIncludeComponent).component);
}
59 changes: 59 additions & 0 deletions lib/modules/manager/gitlabci/extract.spec.ts
@@ -1,3 +1,4 @@
import { codeBlock } from 'common-tags';
import { logger } from '../../../../test/util';
import { GlobalConfig } from '../../../config/global';
import type { RepoGlobalConfig } from '../../../config/types';
Expand Down Expand Up @@ -347,5 +348,63 @@ describe('modules/manager/gitlabci/extract', () => {
expect(extractFromJob(undefined)).toBeEmptyArray();
expect(extractFromJob({ image: 'image:test' })).toEqual(expectedRes);
});

it('extracts component references', () => {
const content = codeBlock`
include:
- component: gitlab.example.com/an-org/a-project/a-component@1.0
inputs:
stage: build
- component: gitlab.example.com/an-org/a-subgroup/a-project/a-component@e3262fdd0914fa823210cdb79a8c421e2cef79d8
- component: gitlab.example.com/an-org/a-subgroup/another-project/a-component@main
- component: gitlab.example.com/another-org/a-project/a-component@~latest
inputs:
stage: test
- component: gitlab.example.com/malformed-component-reference
- component:
malformed: true
- component: gitlab.example.com/an-org/a-component@1.0
- component: other-gitlab.example.com/an-org/a-project/a-component@1.0
`;
const res = extractPackageFile(content, '', {});
expect(res?.deps).toMatchObject([
{
currentValue: '1.0',
datasource: 'gitlab-tags',
depName: 'an-org/a-project',
depType: 'repository',
registryUrls: ['https://gitlab.example.com'],
},
{
currentValue: 'e3262fdd0914fa823210cdb79a8c421e2cef79d8',
datasource: 'gitlab-tags',
depName: 'an-org/a-subgroup/a-project',
depType: 'repository',
registryUrls: ['https://gitlab.example.com'],
},
{
currentValue: 'main',
datasource: 'gitlab-tags',
depName: 'an-org/a-subgroup/another-project',
depType: 'repository',
registryUrls: ['https://gitlab.example.com'],
},
{
currentValue: '~latest',
datasource: 'gitlab-tags',
depName: 'another-org/a-project',
depType: 'repository',
registryUrls: ['https://gitlab.example.com'],
skipReason: 'unsupported-version',
},
{
currentValue: '1.0',
datasource: 'gitlab-tags',
depName: 'an-org/a-project',
depType: 'repository',
registryUrls: ['https://other-gitlab.example.com'],
},
]);
});
});
});
110 changes: 103 additions & 7 deletions lib/modules/manager/gitlabci/extract.ts
@@ -1,18 +1,38 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { readLocalFile } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { trimLeadingSlash } from '../../../util/url';
import { parseSingleYaml } from '../../../util/yaml';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import type {
ExtractConfig,
PackageDependency,
PackageFile,
PackageFileContent,
} from '../types';
import { isGitlabIncludeLocal } from './common';
import type { GitlabPipeline, Image, Job, Services } from './types';
import {
filterIncludeFromGitlabPipeline,
isGitlabIncludeComponent,
isGitlabIncludeLocal,
isNonEmptyObject,
} from './common';
import type {
GitlabInclude,
GitlabIncludeComponent,
GitlabPipeline,
Image,
Job,
Services,
} from './types';
import { getGitlabDep, replaceReferenceTags } from './utils';

// See https://docs.gitlab.com/ee/ci/components/index.html#use-a-component
const componentReferenceRegex = regEx(
/(?<fqdn>[^/]+)\/(?<projectPath>.+)\/(?:.+)@(?<specificVersion>.+)/,
);
const componentReferenceLatestVersion = '~latest';

export function extractFromImage(
image: Image | undefined,
registryAliases?: Record<string, string>,
Expand Down Expand Up @@ -76,6 +96,67 @@ export function extractFromJob(
return deps;
}

function getIncludeComponentsFromInclude(
includeValue: GitlabInclude[] | GitlabInclude,
): GitlabIncludeComponent[] {
const includes = is.array(includeValue) ? includeValue : [includeValue];
return includes.filter(isGitlabIncludeComponent);
}

function getAllIncludeComponents(
data: GitlabPipeline,
): GitlabIncludeComponent[] {
const childrenData = Object.values(filterIncludeFromGitlabPipeline(data))
.filter(isNonEmptyObject)
.map(getAllIncludeComponents)
.flat();

// Process include key.
if (data.include) {
childrenData.push(...getIncludeComponentsFromInclude(data.include));
}
return childrenData;
}

function extractDepFromIncludeComponent(
includeComponent: GitlabIncludeComponent,
): PackageDependency | null {
const componentReference = componentReferenceRegex.exec(
includeComponent.component,
)?.groups;
if (!componentReference) {
logger.debug(
{ componentReference: includeComponent.component },
'Ignoring malformed component reference',
);
return null;
}
const projectPathParts = componentReference.projectPath.split('/');
if (projectPathParts.length < 2) {
logger.debug(
{ componentReference: includeComponent.component },
'Ignoring component reference with incomplete project path',
);
return null;
}

const dep: PackageDependency = {
datasource: GitlabTagsDatasource.id,
depName: componentReference.projectPath,
depType: 'repository',
currentValue: componentReference.specificVersion,
registryUrls: [`https://${componentReference.fqdn}`],
};
if (dep.currentValue === componentReferenceLatestVersion) {
logger.debug(
{ componentVersion: dep.currentValue },
'Ignoring component version',
);
dep.skipReason = 'unsupported-version';
}
return dep;
}

export function extractPackageFile(
content: string,
packageFile: string,
Expand All @@ -84,7 +165,7 @@ export function extractPackageFile(
let deps: PackageDependency[] = [];
try {
// TODO: use schema (#9610)
const doc = parseSingleYaml(replaceReferenceTags(content), {
const doc = parseSingleYaml<GitlabPipeline>(replaceReferenceTags(content), {
json: true,
});
if (is.object(doc)) {
Expand Down Expand Up @@ -115,11 +196,26 @@ export function extractPackageFile(
}
deps = deps.filter(is.truthy);
}

const includedComponents = getAllIncludeComponents(doc);
for (const includedComponent of includedComponents) {
const dep = extractDepFromIncludeComponent(includedComponent);
if (dep) {
deps.push(dep);
}
}
} catch (err) /* istanbul ignore next */ {
logger.debug(
{ err, packageFile },
'Error extracting GitLab CI dependencies',
);
if (err.stack?.startsWith('YAMLException:')) {
logger.debug(
{ err, packageFile },
'YAML exception extracting GitLab CI includes',
);
} else {
logger.debug(
{ err, packageFile },
'Error extracting GitLab CI dependencies',
);
}
}

return deps.length ? { deps } : null;
Expand Down
6 changes: 5 additions & 1 deletion lib/modules/manager/gitlabci/index.ts
@@ -1,5 +1,6 @@
import type { Category } from '../../../constants';
import { DockerDatasource } from '../../datasource/docker';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import { extractAllPackageFiles, extractPackageFile } from './extract';

export { extractAllPackageFiles, extractPackageFile };
Expand All @@ -10,4 +11,7 @@ export const defaultConfig = {

export const categories: Category[] = ['ci'];

export const supportedDatasources = [DockerDatasource.id];
export const supportedDatasources = [
DockerDatasource.id,
GitlabTagsDatasource.id,
];
5 changes: 5 additions & 0 deletions lib/modules/manager/gitlabci/types.ts
Expand Up @@ -16,6 +16,10 @@ export interface GitlabIncludeTemplate {
template: string;
}

export interface GitlabIncludeComponent {
component: string;
}

export interface GitlabPipeline {
include?: GitlabInclude[] | GitlabInclude;
}
Expand All @@ -39,4 +43,5 @@ export type GitlabInclude =
| GitlabIncludeProject
| GitlabIncludeRemote
| GitlabIncludeTemplate
| GitlabIncludeComponent
| string;

0 comments on commit edea11d

Please sign in to comment.