@@ -26,14 +26,31 @@ type ParserIncludesInitOptions = {
2626} ;
2727
2828type ParsedComponent = {
29+ _cache : {
30+ version : string | null | undefined ;
31+ effectiveRef : string | undefined ;
32+ sha : string | undefined ;
33+ } ;
34+ gitData : GitData ;
2935 domain : string ;
3036 port : string ;
3137 projectPath : string ;
38+ componentPath : string ;
3239 name : string ;
33- ref : string ;
40+ reference : string ;
41+ version : string | null ;
42+ effectiveRef : string ;
43+ sha : string ;
3444 isLocal : boolean ;
3545} ;
3646
47+ type GitRemoteInfoContext = {
48+ gitData : GitData ;
49+ domain : string ;
50+ port : string ;
51+ projectPath : string ;
52+ } ;
53+
3754export class ParserIncludes {
3855 private static count : number = 0 ;
3956
@@ -100,7 +117,7 @@ export class ParserIncludes {
100117 componentParseCache . set ( index , component ) ;
101118 if ( ! component . isLocal )
102119 {
103- promises . push ( this . downloadIncludeComponent ( opts , component . projectPath , component . ref , component . name ) ) ;
120+ promises . push ( this . downloadIncludeComponent ( opts , component . projectPath , component . effectiveRef , component . componentPath ) ) ;
104121 }
105122 }
106123
@@ -142,13 +159,13 @@ export class ParserIncludes {
142159 const component = componentParseCache . get ( index ) ;
143160 assert ( component !== undefined , `Internal error, component parse cache missing entry [${ index } ]` ) ;
144161 // Gitlab allows two different file paths to include a component
145- const files = [ `${ component . name } .yml` , `${ component . name } /template.yml` ] ;
162+ const files = [ `${ component . componentPath } .yml` , `${ component . componentPath } /template.yml` ] ;
146163
147164 let file = null ;
148165 for ( const f of files ) {
149166 let searchPath = `${ cwd } /${ f } ` ;
150167 if ( ! component . isLocal ) {
151- searchPath = `${ cwd } /${ stateDir } /includes/${ gitData . remote . host } /${ component . projectPath } /${ component . ref } /${ f } ` ;
168+ searchPath = `${ cwd } /${ stateDir } /includes/${ gitData . remote . host } /${ component . projectPath } /${ component . effectiveRef } /${ f } ` ;
152169 }
153170 if ( fs . existsSync ( searchPath ) ) {
154171 file = searchPath ;
@@ -158,13 +175,15 @@ export class ParserIncludes {
158175 ( component . port ? `:${ component . port } ` : "" ) + `/${ component . projectPath } \`` ) ;
159176
160177 // Extract component name for component-specific inputs
161- const componentName = component . name . replace ( / ^ t e m p l a t e s \/ / , "" ) ;
178+ const componentName = component . componentPath . replace ( / ^ t e m p l a t e s \/ / , "" ) ;
162179 const fileComponentInputs = isStructured ? ( fileInputs [ componentName ] ?? { } ) : { } ;
163180 const cliComponentSpecificInputs = cliComponentInputs [ componentName ] ?? { } ;
164181 const mergedInputs = { ...( value . inputs ?? { } ) , ...globalInputs , ...fileComponentInputs , ...cliComponentSpecificInputs } ;
165- const fileDoc = await Parser . loadYaml ( file , { inputs : mergedInputs } , expandVariables , writeStreams ) ;
166- // Expand local includes inside to a "project"-like include
167- fileDoc [ "include" ] = this . expandInnerLocalIncludes ( fileDoc [ "include" ] , component . projectPath , component . ref , opts ) ;
182+ const fileDoc = await Parser . loadYaml ( file , { inputs : mergedInputs , component} , expandVariables , writeStreams ) ;
183+ if ( ! component . isLocal ) {
184+ // Expand local includes inside to a "project"-like include
185+ fileDoc [ "include" ] = this . expandInnerLocalIncludes ( fileDoc [ "include" ] , component . projectPath , component . effectiveRef , opts ) ;
186+ }
168187 includeDatas = includeDatas . concat ( await this . init ( fileDoc , opts ) ) ;
169188 } else if ( value [ "template" ] ) {
170189 const { project, ref, file, domain} = this . covertTemplateToProjectFile ( value [ "template" ] ) ;
@@ -236,42 +255,70 @@ export class ParserIncludes {
236255 assert ( ! component . includes ( "://" ) , `This GitLab CI configuration is invalid: component: \`${ component } \` should not contain protocol` ) ;
237256 const pattern = / (?< domain > [ ^ / : \s ] + ) ( : (?< port > \d + ) ) ? \/ (?< projectPath > .+ ) \/ (?< componentName > [ ^ @ ] + ) @ (?< ref > .+ ) / ; // https://regexr.com/7v7hm
238257 const gitRemoteMatch = pattern . exec ( component ) ;
239-
240258 if ( gitRemoteMatch ?. groups == null ) throw new Error ( `This is a bug, please create a github issue if this is something you're expecting to work. input: ${ component } ` ) ;
241-
242- const { domain, projectPath, port} = gitRemoteMatch . groups ;
243- let ref = gitRemoteMatch . groups [ "ref" ] ;
259+ const { domain, projectPath, port, componentName, ref} = gitRemoteMatch . groups ;
244260 const isLocalComponent = projectPath === `${ gitData . remote . group } /${ gitData . remote . project } ` && ref === gitData . commit . SHA ;
245-
246- if ( ! isLocalComponent ) {
247- const semanticVersionRangesPattern = / ^ \d + ( \. \d + ) ? $ / ;
248- if ( ref == "~latest" || semanticVersionRangesPattern . test ( ref ) ) {
249- // https://docs.gitlab.com/ci/components/#semantic-version-ranges
250- let stdout ;
251- if ( gitData . remote . schema == "git" || gitData . remote . schema == "ssh" ) {
252- stdout = Utils . syncSpawn ( [ "git" , "ls-remote" , "--tags" , `git@${ domain } :${ projectPath } ` ] ) . stdout ;
253- } else {
254- stdout = Utils . syncSpawn ( [ "git" , "ls-remote" , "--tags" , `${ gitData . remote . schema } ://${ domain } :${ port ?? 443 } /${ projectPath } .git` ] ) . stdout ;
255- }
256- assert ( stdout ) ;
257- const tags = stdout
258- . split ( "\n" )
259- . map ( ( line ) => {
260- return line
261- . split ( "\t" ) [ 1 ]
262- . split ( "/" ) [ 2 ] ;
263- } ) ;
264- const _ref = resolveSemanticVersionRange ( ref , tags ) ;
265- assert ( _ref , `This GitLab CI configuration is invalid: component: \`${ component } \` - The ref (${ ref } ) is invalid` ) ;
266- ref = _ref ;
267- }
268- }
269261 return {
270- domain : domain ,
271- port : port ,
272- projectPath : projectPath ,
273- name : `templates/${ gitRemoteMatch . groups [ "componentName" ] } ` ,
274- ref : ref ,
262+ _cache : {
263+ version : undefined ,
264+ effectiveRef : undefined ,
265+ sha : undefined ,
266+ } ,
267+ gitData,
268+ domain,
269+ port,
270+ projectPath,
271+ componentPath : `templates/${ componentName } ` ,
272+ name : componentName ,
273+ reference : ref ,
274+ get version ( ) {
275+ if ( this . _cache . version === undefined ) {
276+ if ( this . isLocal ) {
277+ this . _cache . version = this . gitData . commit . SHA ;
278+ } else {
279+ const semanticVersionRangesPattern = / ^ \d + ( \. \d + ) ? $ / ;
280+ if ( this . reference == "~latest" || semanticVersionRangesPattern . test ( this . reference ) ) {
281+ // https://docs.gitlab.com/ci/components/#semantic-version-ranges
282+ const stdout = getGitRemoteInfo ( this , "--tags" ) ;
283+ const tags = stdout . split ( "\n" ) . map ( line => line . split ( "\t" ) [ 1 ] . split ( "/" ) [ 2 ] ) ;
284+ const version = resolveSemanticVersionRange ( this . reference , tags ) ;
285+ assert ( version , `This GitLab CI configuration is invalid: component: \`${ this . name } \` - The reference (${ this . reference } ) is invalid` ) ;
286+ this . _cache . version = version ;
287+ } else {
288+ this . _cache . version = null ;
289+ }
290+ }
291+ }
292+ return this . _cache . version ;
293+ } ,
294+ get effectiveRef ( ) {
295+ if ( this . _cache . effectiveRef === undefined ) {
296+ this . _cache . effectiveRef = this . version ?? this . reference ;
297+ }
298+ return this . _cache . effectiveRef ;
299+ } ,
300+ get sha ( ) {
301+ if ( this . _cache . sha === undefined ) {
302+ if ( this . isLocal ) {
303+ this . _cache . sha = this . gitData . commit . SHA ;
304+ } else if ( / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( this . effectiveRef ) ) {
305+ // effectiveRef may already be a sha, if so return it directly
306+ this . _cache . sha = this . effectiveRef ;
307+ } else {
308+ const stdout = getGitRemoteInfo ( this ) ;
309+ const lines = stdout . split ( "\n" ) ;
310+ // annotated tags: prefer the deref'd commit sha (refs/tags/x^{})
311+ const match = lines . find ( line => line . endsWith ( `refs/tags/${ this . effectiveRef } ^{}` ) ) ??
312+ lines . find ( line =>
313+ line . endsWith ( `refs/tags/${ this . effectiveRef } ` ) ||
314+ line . endsWith ( `refs/heads/${ this . effectiveRef } ` ) ,
315+ ) ;
316+ assert ( match , `Could not resolve commit SHA for ${ this . effectiveRef } in ${ this . projectPath } ` ) ;
317+ this . _cache . sha = match . split ( "\t" ) [ 0 ] ;
318+ }
319+ }
320+ return this . _cache . sha ;
321+ } ,
275322 isLocal : isLocalComponent ,
276323 } ;
277324 }
@@ -458,3 +505,13 @@ export async function resolveIncludeLocal (pattern: string, cwd: string) {
458505 const re2js = RE2JS . compile ( `^${ pattern } ` ) ;
459506 return repoFiles . filter ( ( f : any ) => re2js . matches ( f ) ) ;
460507}
508+
509+ export function getGitRemoteInfo ( ctx : GitRemoteInfoContext , ...args : string [ ] ) {
510+ const cmdArgs = [ "git" , "ls-remote" , ...args ] ;
511+ if ( ctx . gitData . remote . schema == "git" || ctx . gitData . remote . schema == "ssh" ) {
512+ cmdArgs . push ( `git@${ ctx . domain } :${ ctx . projectPath } ` ) ;
513+ } else {
514+ cmdArgs . push ( `${ ctx . gitData . remote . schema } ://${ ctx . domain } :${ ctx . port ?? 443 } /${ ctx . projectPath } .git` ) ;
515+ }
516+ return Utils . syncSpawn ( cmdArgs ) . stdout ;
517+ }
0 commit comments