@@ -18,6 +18,7 @@ import {Parser} from "./parser.js";
1818import { resolveIncludeLocal , validateIncludeLocal } from "./parser-includes.js" ;
1919import globby from "globby" ;
2020import terminalLink from "terminal-link" ;
21+ import * as crypto from "crypto" ;
2122
2223const GCL_SHELL_PROMPT_PLACEHOLDER = "<gclShellPromptPlaceholder>" ;
2324interface JobOptions {
@@ -207,7 +208,12 @@ export class Job {
207208 // Find environment matched variables
208209 if ( this . environment && expandVariables ) {
209210 const expanded = Utils . expandVariables ( this . _variables ) ;
211+ const envNameBeforeExpansion = this . environment . name ;
210212 this . environment . name = Utils . expandText ( this . environment . name , expanded ) ;
213+ if ( this . environment . name !== envNameBeforeExpansion ) {
214+ // Regenerate CI_ENVIRONMENT_SLUG based on env name if it changed after expansion
215+ predefinedVariables [ "CI_ENVIRONMENT_SLUG" ] = this . _generateEnvironmentSlug ( this . environment . name ) ;
216+ }
211217 this . environment . url = Utils . expandText ( this . environment . url , expanded ) ;
212218 }
213219 const envMatchedVariables = Utils . findEnvMatchedVariables ( variablesFromFiles , this . fileVariablesDir , this . environment ) ;
@@ -314,7 +320,7 @@ export class Job {
314320 predefinedVariables [ "CI_JOB_URL" ] = `${ predefinedVariables [ "CI_SERVER_URL" ] } /${ gitData . remote . group } /${ gitData . remote . project } /-/jobs/${ this . jobId } ` ; // Changes on rerun.
315321 predefinedVariables [ "CI_PIPELINE_URL" ] = `${ predefinedVariables [ "CI_SERVER_URL" ] } /${ gitData . remote . group } /${ gitData . remote . project } /pipelines/${ this . pipelineIid } ` ;
316322 predefinedVariables [ "CI_ENVIRONMENT_NAME" ] = this . environment ?. name ?? "" ;
317- predefinedVariables [ "CI_ENVIRONMENT_SLUG" ] = this . environment ?. name ?. replace ( / [ ^ a - z \d ] + / ig , "-" ) . replace ( / ^ - / , "" ) . slice ( 0 , 23 ) . replace ( / - $ / , "" ) . toLowerCase ( ) ?? "" ;
323+ predefinedVariables [ "CI_ENVIRONMENT_SLUG" ] = this . environment ?. name ? this . _generateEnvironmentSlug ( this . environment . name ) : "" ;
318324 predefinedVariables [ "CI_ENVIRONMENT_URL" ] = this . environment ?. url ?? "" ;
319325 predefinedVariables [ "CI_ENVIRONMENT_TIER" ] = this . environment ?. deployment_tier ?? "" ;
320326 predefinedVariables [ "CI_ENVIRONMENT_ACTION" ] = this . environment ?. action ?? "" ;
@@ -328,6 +334,57 @@ export class Job {
328334 return predefinedVariables ;
329335 }
330336
337+ /**
338+ * Generates a compliant slug for an environment name.
339+ * See: https://gitlab.com/gitlab-org/gitlab/-/blob/fc31e7ac344e53ebae182ea1dca183bdc0e2ea71/lib/gitlab/slug/environment.rb
340+ *
341+ * The slug:
342+ * - Contains only lowercase letters (a-z), numbers (0-9), and '-'.
343+ * - Begins with a letter.
344+ * - Has a maximum length of 24 characters.
345+ * - Does not end with '-'.
346+ *
347+ * @param name The original environment name.
348+ * @returns A compliant environment slug.
349+ */
350+ private _generateEnvironmentSlug ( name : string ) : string {
351+ // 1. Lowercase, replace non-alphanumeric with '-', and squeeze repeating '-'
352+ let slug = name
353+ . toLowerCase ( )
354+ . replace ( / [ ^ a - z 0 - 9 ] / g, "-" )
355+ . replace ( / - + / g, "-" ) ;
356+
357+ // 2. Must start with a letter
358+ if ( ! / ^ [ a - z ] / . test ( slug ) ) {
359+ slug = `env-${ slug } ` ;
360+ }
361+
362+ // 3. If it's too long or was modified, shorten and add a hash suffix
363+ if ( slug . length > 24 || slug !== name ) {
364+ // Truncate to 17 chars (leaving room for '-' + 6-char hash)
365+ slug = slug . slice ( 0 , 17 ) ;
366+
367+ // Ensure it ends with a dash before adding the suffix
368+ if ( ! slug . endsWith ( "-" ) ) {
369+ slug += "-" ;
370+ }
371+
372+ // Create the 6-char suffix from a hash of the *original* name
373+ const hexHash = crypto
374+ . createHash ( "sha256" )
375+ . update ( name )
376+ . digest ( "hex" ) ;
377+
378+ // Use BigInt for safe conversion from hex -> base36
379+ const suffix = BigInt ( `0x${ hexHash } ` ) . toString ( 36 ) . slice ( - 6 ) ;
380+
381+ return slug + suffix ;
382+ }
383+
384+ // 4. If it was short and unmodified, just ensure it doesn't end with '-'
385+ return slug . replace ( / - $ / , "" ) ;
386+ }
387+
331388 get jobStatus ( ) {
332389 if ( this . preScriptsExitCode == null ) return "pending" ;
333390 if ( this . preScriptsExitCode == 0 ) return "success" ;
0 commit comments