Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gitlab): support GitLab CI/CD component references #26660

Merged
merged 24 commits into from Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7458246
feat(gitlab): support GitLab CI/CD component references
fgreinacher Jan 16, 2024
3384d43
Merge branch 'main' into feat/cicd-components
fgreinacher Jan 16, 2024
f2e6482
refactor: reject incomplete project paths
fgreinacher Jan 16, 2024
f784b6c
refactor: ignore external component references
fgreinacher Jan 16, 2024
36fbbc6
refactor: rename variable
fgreinacher Jan 16, 2024
d5107b0
Merge branch 'main' into feat/cicd-components
fgreinacher Jan 16, 2024
144978f
refactor: remove unneeded optional accessor
fgreinacher Jan 17, 2024
12a5a4f
refactor: replace snapshot by inline object
fgreinacher Jan 17, 2024
8e79c98
refactor: remove unneeded array handling
fgreinacher Jan 17, 2024
457c124
refactor: simplify regex matching
fgreinacher Jan 17, 2024
acb0b13
Merge branch 'main' into feat/cicd-components
fgreinacher Jan 17, 2024
80b0211
Merge branch 'main' into feat/cicd-components
fgreinacher Jan 17, 2024
c3a6318
refactor: set registry URL for external component reference
fgreinacher Jan 17, 2024
ef932dd
refactor: move new logic to gitlabci manager
fgreinacher Jan 17, 2024
14b9e30
test: adapt fixtures
fgreinacher Jan 17, 2024
08b3044
Merge remote-tracking branch 'upstream/main' into feat/cicd-components
fgreinacher Jan 19, 2024
5d82bc1
test: add missing test case
fgreinacher Jan 19, 2024
c11ba71
refactor: delete unneeded fixture
fgreinacher Jan 19, 2024
e61f617
fix: update supported datasources
fgreinacher Jan 19, 2024
dac0433
docs: remove wrong comment
fgreinacher Jan 19, 2024
2e37a6e
refactor: inline variable
fgreinacher Jan 20, 2024
3cbce5a
refactor: always set registryUrls
fgreinacher Jan 20, 2024
ec13858
Update extract.ts
fgreinacher Jan 20, 2024
91a4d20
Merge branch 'main' into feat/cicd-components
fgreinacher Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/modules/manager/gitlabci-include/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-include/extract.spec.ts
@@ -1,3 +1,4 @@
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../test/fixtures';
import { GlobalConfig } from '../../../config/global';
import { extractPackageFile } from '.';
Expand Down Expand Up @@ -56,6 +57,64 @@ describe('modules/manager/gitlabci-include/extract', () => {
expect(res).toBeNull();
});

it('extracts component references', () => {
GlobalConfig.set({
endpoint: 'https://gitlab.example.com',
});

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',
},
{
currentValue: 'e3262fdd0914fa823210cdb79a8c421e2cef79d8',
datasource: 'gitlab-tags',
depName: 'an-org/a-subgroup/a-project',
depType: 'repository',
},
{
currentValue: 'main',
datasource: 'gitlab-tags',
depName: 'an-org/a-subgroup/another-project',
depType: 'repository',
},
{
currentValue: '~latest',
datasource: 'gitlab-tags',
depName: 'another-org/a-project',
depType: 'repository',
skipReason: 'unsupported-version',
},
{
currentValue: '1.0',
datasource: 'gitlab-tags',
depName: 'an-org/a-project',
depType: 'repository',
skipReason: 'invalid-value',
},
]);
});

it('normalizes configured endpoints', () => {
const endpoints = [
'http://gitlab.test/api/v4',
Expand Down
98 changes: 98 additions & 0 deletions lib/modules/manager/gitlabci-include/extract.ts
Expand Up @@ -2,21 +2,30 @@
import { GlobalConfig } from '../../../config/global';
import { logger } from '../../../logger';
import { regEx } from '../../../util/regex';
import { parseUrl } from '../../../util/url';
import { parseSingleYaml } from '../../../util/yaml';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags';
import type {
GitlabInclude,
GitlabIncludeComponent,
GitlabIncludeProject,
GitlabPipeline,
} from '../gitlabci/types';
import { replaceReferenceTags } from '../gitlabci/utils';
import type { PackageDependency, PackageFileContent } from '../types';
import {
filterIncludeFromGitlabPipeline,
isGitlabIncludeComponent,
isGitlabIncludeProject,
isNonEmptyObject,
} from './common';

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

function extractDepFromIncludeFile(
includeObj: GitlabIncludeProject,
): PackageDependency {
Expand All @@ -33,6 +42,57 @@
return dep;
}

function extractDepFromIncludeComponent(
includeComponent: GitlabIncludeComponent,
endpoint: string | undefined,
): PackageDependency | null {
const componentReferenceMatch = componentReferenceRegex.exec(
includeComponent.component,
);
if (!componentReferenceMatch?.groups) {
viceice marked this conversation as resolved.
Show resolved Hide resolved
fgreinacher marked this conversation as resolved.
Show resolved Hide resolved
logger.debug(
{ componentReference: includeComponent.component },
'Ignoring malformed component reference',
);
return null;
}
const projectPathParts =
componentReferenceMatch.groups.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: componentReferenceMatch.groups.projectPath,
depType: 'repository',
currentValue: componentReferenceMatch.groups.specificVersion,
};
if (dep.currentValue === componentReferenceLatestVersion) {
logger.debug(
{ componentVersion: dep.currentValue },
'Ignoring component version',
);
dep.skipReason = 'unsupported-version';
}
rarkins marked this conversation as resolved.
Show resolved Hide resolved
const endpointUrl = parseUrl(endpoint);
if (
endpointUrl &&
endpointUrl.hostname !== componentReferenceMatch.groups.fqdn
) {
logger.debug(
{ componentReference: includeComponent.component },
'Ignoring external component reference',
);
dep.skipReason = 'invalid-value';
fgreinacher marked this conversation as resolved.
Show resolved Hide resolved
}
return dep;
}

function getIncludeProjectsFromInclude(
includeValue: GitlabInclude[] | GitlabInclude,
): GitlabIncludeProject[] {
Expand Down Expand Up @@ -63,6 +123,37 @@
return childrenData;
}

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

// Filter out includes that dont have a file & project.
return includes.filter(isGitlabIncludeComponent);
}
function getAllIncludeComponents(
data: GitlabPipeline,
): GitlabIncludeComponent[] {
// If Array, search each element.
if (is.array(data)) {
fgreinacher marked this conversation as resolved.
Show resolved Hide resolved
return (data as GitlabPipeline[])
.filter(isNonEmptyObject)
.map(getAllIncludeComponents)
.flat();
}

const childrenData = Object.values(filterIncludeFromGitlabPipeline(data))
.filter(isNonEmptyObject)
.map(getAllIncludeComponents)
.flat();

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

export function extractPackageFile(
content: string,
packageFile?: string,
Expand All @@ -71,7 +162,7 @@
const platform = GlobalConfig.get('platform');
const endpoint = GlobalConfig.get('endpoint');
try {
const doc = parseSingleYaml(replaceReferenceTags(content), {

Check failure on line 165 in lib/modules/manager/gitlabci-include/extract.ts

View workflow job for this annotation

GitHub Actions / lint-eslint

This assertion is unnecessary since it does not change the type of the expression.
json: true,
}) as GitlabPipeline;
const includes = getAllIncludeProjects(doc);
Expand All @@ -82,6 +173,13 @@
}
deps.push(dep);
}
const includedComponents = getAllIncludeComponents(doc);
for (const includedComponent of includedComponents) {
const dep = extractDepFromIncludeComponent(includedComponent, endpoint);
if (dep) {
deps.push(dep);
}
}
} catch (err) /* istanbul ignore next */ {
if (err.stack?.startsWith('YAMLException:')) {
logger.debug(
Expand Down
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;