diff --git a/lib/modules/manager/gitlabci-include/__snapshots__/extract.spec.ts.snap b/lib/modules/manager/gitlabci-include/__snapshots__/extract.spec.ts.snap index 986b82e20fb333..a7f75109d426d7 100644 --- a/lib/modules/manager/gitlabci-include/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/gitlabci-include/__snapshots__/extract.spec.ts.snap @@ -1,5 +1,42 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`modules/manager/gitlabci-include/extract extractPackageFile() extracts component references 1`] = ` +[ + { + "datasource": "gitlab-tags", + "depName": "an-org/a-project", + "depType": "repository", + "registryUrls": [ + "gitlab.example.com", + ], + }, + { + "datasource": "gitlab-tags", + "depName": "an-org/a-subgroup/a-project", + "depType": "repository", + "registryUrls": [ + "gitlab.example.com", + ], + }, + { + "datasource": "gitlab-tags", + "depName": "an-org/a-subgroup/another-project", + "depType": "repository", + "registryUrls": [ + "gitlab.example.com", + ], + }, + { + "datasource": "gitlab-tags", + "depName": "another-org/a-project", + "depType": "repository", + "registryUrls": [ + "gitlab.example.com", + ], + }, +] +`; + exports[`modules/manager/gitlabci-include/extract extractPackageFile() extracts multiple include blocks 1`] = ` [ { diff --git a/lib/modules/manager/gitlabci-include/common.ts b/lib/modules/manager/gitlabci-include/common.ts index 7aefb628e62d00..38452d1b50a954 100644 --- a/lib/modules/manager/gitlabci-include/common.ts +++ b/lib/modules/manager/gitlabci-include/common.ts @@ -1,6 +1,7 @@ import is from '@sindresorhus/is'; import type { GitlabInclude, + GitlabIncludeComponent, GitlabIncludeLocal, GitlabIncludeProject, GitlabPipeline, @@ -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); +} diff --git a/lib/modules/manager/gitlabci-include/extract.spec.ts b/lib/modules/manager/gitlabci-include/extract.spec.ts index 9ebd9b8c572807..c30851f1d74c54 100644 --- a/lib/modules/manager/gitlabci-include/extract.spec.ts +++ b/lib/modules/manager/gitlabci-include/extract.spec.ts @@ -56,6 +56,24 @@ describe('modules/manager/gitlabci-include/extract', () => { expect(res).toBeNull(); }); + it('extracts component references', () => { + const includeWithoutProjectRef = `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`; + const res = extractPackageFile(includeWithoutProjectRef); + expect(res?.deps).toMatchSnapshot(); + expect(res?.deps).toHaveLength(4); + }); + it('normalizes configured endpoints', () => { const endpoints = [ 'http://gitlab.test/api/v4', diff --git a/lib/modules/manager/gitlabci-include/extract.ts b/lib/modules/manager/gitlabci-include/extract.ts index 95a98dc87af54d..77d0deb242185a 100644 --- a/lib/modules/manager/gitlabci-include/extract.ts +++ b/lib/modules/manager/gitlabci-include/extract.ts @@ -6,6 +6,7 @@ import { parseSingleYaml } from '../../../util/yaml'; import { GitlabTagsDatasource } from '../../datasource/gitlab-tags'; import type { GitlabInclude, + GitlabIncludeComponent, GitlabIncludeProject, GitlabPipeline, } from '../gitlabci/types'; @@ -13,10 +14,17 @@ 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( + /(?[^/]+)\/(?.+)\/(?.+)@(?.+)/, +); +const componentReferenceLatestVersion = '~latest'; + function extractDepFromIncludeFile( includeObj: GitlabIncludeProject, ): PackageDependency { @@ -33,6 +41,36 @@ function extractDepFromIncludeFile( return dep; } +function extractDepFromIncludeComponent( + includeComponent: GitlabIncludeComponent, +): PackageDependency | null { + const componentReferenceMatch = componentReferenceRegex?.exec( + includeComponent.component, + ); + if (!componentReferenceMatch?.groups) { + logger.debug( + { componentReference: includeComponent.component }, + 'Ignoring malformed component reference', + ); + return null; + } + const dep: PackageDependency = { + datasource: GitlabTagsDatasource.id, + depName: componentReferenceMatch.groups.projectPath, + depType: 'repository', + + registryUrls: [componentReferenceMatch.groups.fqdn], + }; + if (dep.currentValue === componentReferenceLatestVersion) { + logger.debug( + { componentVersion: dep.currentValue }, + 'Ignoring component version', + ); + dep.skipReason = 'unsupported-version'; + } + return dep; +} + function getIncludeProjectsFromInclude( includeValue: GitlabInclude[] | GitlabInclude, ): GitlabIncludeProject[] { @@ -63,6 +101,37 @@ function getAllIncludeProjects(data: GitlabPipeline): GitlabIncludeProject[] { 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)) { + 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, @@ -82,6 +151,13 @@ export function extractPackageFile( } deps.push(dep); } + const includedComponents = getAllIncludeComponents(doc); + for (const includedComponent of includedComponents) { + const dep = extractDepFromIncludeComponent(includedComponent); + if (dep) { + deps.push(dep); + } + } } catch (err) /* istanbul ignore next */ { if (err.stack?.startsWith('YAMLException:')) { logger.debug( diff --git a/lib/modules/manager/gitlabci/types.ts b/lib/modules/manager/gitlabci/types.ts index 9f8d7726b2f472..3728b393bfcd36 100644 --- a/lib/modules/manager/gitlabci/types.ts +++ b/lib/modules/manager/gitlabci/types.ts @@ -16,6 +16,10 @@ export interface GitlabIncludeTemplate { template: string; } +export interface GitlabIncludeComponent { + component: string; +} + export interface GitlabPipeline { include?: GitlabInclude[] | GitlabInclude; } @@ -39,4 +43,5 @@ export type GitlabInclude = | GitlabIncludeProject | GitlabIncludeRemote | GitlabIncludeTemplate + | GitlabIncludeComponent | string;